mysql的rr隔离级别无法完全避免幻读,因其依赖next-key lock仅在当前读+索引+范围查询时生效,而快照读不加锁、等值查询只加记录锁、无索引时锁失效,混用快照读与当前读导致视图不一致。

快照读不加锁,但读的不是最新数据
快照读是普通 SELECT 的默认行为,它不申请任何锁,也不会阻塞其他事务的写操作。但它读的不是磁盘或 Buffer Pool 里的最新值,而是根据事务启动时生成的 Read View,顺着 Undo Log 版本链找到“该事务可见”的历史版本。
这导致一个典型现象:在 REPEATABLE READ 隔离级别下,事务内多次执行 SELECT * FROM t WHERE id = 1,结果始终一致,哪怕其他事务已提交了修改——因为每次都复用同一个 Read View。
-
READ COMMITTED下每次SELECT都会新建Read View,所以能读到其他事务刚COMMIT的新值(不可重复读) - 快照读无法感知幻读问题中的新插入行,但 InnoDB 用 MVCC 本身就能在快照读场景下避免幻读(依赖一致性视图)
- 不要误以为“没加锁=没开销”——Undo Log 链遍历、可见性判断都有 CPU 开销,尤其在长事务+大量更新时
当前读强制读最新版,并立即加锁
所有写操作(INSERT / UPDATE / DELETE)和显式加锁查询(SELECT ... FOR UPDATE / SELECT ... LOCK IN SHARE MODE)都触发当前读。它绕过 MVCC 快照,直接访问数据页最新状态,并按需加锁:
-
UPDATE和DELETE加排他锁(X 锁),阻止其他事务读写该行 -
SELECT ... FOR UPDATE同样加 X 锁;SELECT ... LOCK IN SHARE MODE加共享锁(S 锁),允许并发读但阻塞写 - 当前读一定会触发
next-key lock(行锁 + 间隙锁),这是 RR 级别下防止幻读的关键机制 - 即使你只查一条主键记录,InnoDB 仍可能锁住索引间隙——比如
WHERE id = 5当前读,会同时锁住 (3,5) 和 (5,7) 这两个间隙
为什么 UPDATE 语句里藏着一次当前读
UPDATE t SET name = 'x' WHERE id = 1 看似只是写操作,实际分两步:先读(当前读),再改。这一步读必须拿到最新值并加锁,否则可能覆盖其他事务的变更。
- 如果此时另一事务刚
DELETE FROM t WHERE id = 1并COMMIT,当前读能立刻发现该行已不存在,从而避免误更新 - 若误用快照读(比如在存储过程中错误地把
SELECT结果缓存后延迟UPDATE),就可能基于过期快照执行,造成数据错乱 - 注意:
UPDATE的 WHERE 条件匹配过程本身也走当前读,所以UPDATE ... WHERE status = 'pending'可能锁住多行,甚至触发间隙锁范围扩大
混合使用时最容易忽略的锁等待陷阱
一个事务里混用快照读和当前读,锁行为不会“继承”或“抵消”。比如先执行 SELECT * FROM t WHERE id = 1(快照读,无锁),再执行 SELECT * FROM t WHERE id = 1 FOR UPDATE(当前读,加 X 锁),后者依然会等——哪怕前者刚读过同一行。
- 快照读释放的“无锁”状态不等于“已释放资源”,它根本不参与锁管理,所以对后续当前读毫无影响
- 不同隔离级别下,
SELECT ... FOR UPDATE在 RR 和 RC 中加锁范围不同:RR 用next-key lock,RC 只加行锁,不锁间隙——这意味着 RC 下可能产生幻读 - 开发中常见误区:以为“只读不写就安全”,但一旦在事务里出现任意当前读,整个事务就进入了锁敏感路径,必须考虑死锁风险
真正麻烦的从来不是概念区分,而是某条看似普通的 SELECT 因为加了 FOR UPDATE 就让整条业务链路串行化,而你直到压测时才看到锁等待超时。











