MySQL在RR级别下仍可能发生幻读,根本原因是RR仅通过MVCC保证快照读一致性,而当前读会暴露新插入行;混用快照读与当前读、Next-Key Lock未生效或执行计划变化均会导致语义幻读。

MySQL 在 RR 隔离级别下依然可能发生幻读,根本原因不是“InnoDB 没做好”,而是 RR 本身不承诺消灭所有幻读场景——它只在快照读(普通 SELECT)中靠 MVCC 隐蔽了幻影行;一旦混入当前读(如 UPDATE、SELECT ... FOR UPDATE),幻读立刻暴露。
快照读和当前读混用必然导致结果不一致
这是生产中最常翻车的点:同一个事务里,先执行一次普通 SELECT(快照读),再执行 UPDATE ... WHERE x > 10(当前读),两次看到的数据集合可能不同。
- 第一次
SELECT * FROM t WHERE id > 10返回 5 行 → 基于事务启动时生成的ReadView,看不到其他事务新插入的行 - 另一事务插入
id = 15并提交 - 本事务执行
UPDATE t SET status = 'done' WHERE id > 10→ 当前读穿透 MVCC,读到最新数据,实际更新 6 行 - 再执行
SELECT * FROM t WHERE id > 10→ 仍返回 5 行(复用旧ReadView)
业务逻辑若基于第一次查询结果做判断(比如“只有 5 条待处理,可以发 5 条通知”),但实际更新了 6 条,就构成语义层面的幻读——不是数据库报错,而是应用逻辑被悄悄破坏。
Next-Key Lock 只在特定当前读中生效
Next-Key Lock 是 InnoDB 防幻读的核心机制,但它有严格触发条件,不是“只要 RR 就自动启用”。
- 必须是当前读语句:
SELECT ... FOR UPDATE、UPDATE、DELETE,普通SELECT不触发 - WHERE 条件必须能走索引,且最好是范围查询(如
id BETWEEN 10 AND 20);等值查询(id = 15)只加记录锁,不锁间隙 - 若字段无索引(如
name = 'Alice'),InnoDB 退化为全表扫描,Next-Key Lock失效,仅加行锁或意向锁 - 非唯一二级索引上等值查询(如
INDEX(b),b=10 存在多行),InnoDB 会向右扫描直到第一个不满足的值,锁住更大范围(如(10, 15]),但这不是开发者能直观预期的
间隙锁的加锁范围取决于 B+ 树遍历路径
Next-Key Lock 不是静态锁定某几行,而是动态决定锁哪些 (gap_start, record] 区间,完全依赖索引结构和查询执行计划。
- 表中主键
id当前值为 10、20、30,则SELECT * FROM t WHERE id > 20 FOR UPDATE会锁(20, 30]和(30, +∞) - 但
SELECT * FROM t WHERE id = 25 FOR UPDATE(无对应记录)只锁间隙(20, 30),不锁任何记录 - 如果查询条件无法命中索引(例如隐式类型转换:
WHERE id = '25'而id是INT),索引失效,间隙锁直接消失
这意味着,你写的 SQL 看似一样,只要执行计划稍有变化(如统计信息过期、索引被删、参数类型不匹配),加锁行为就可能完全不同,幻读风险随之浮现。
真正可控的防幻读手段只有显式加锁
依赖 RR 默认行为来防幻读,等于把一致性交给运气。要业务级可靠,必须主动控制读取模式:
- 所有需要强一致性的范围判断,统一改用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE,让 MVCC 让位于锁机制 - 避免在单个事务内混用普通
SELECT和当前读——要么全程快照读(接受其“看不见新数据”的局限),要么全程当前读(用锁兜底) - 若必须“先查后更”,建议事务开始后立即执行一次带锁的当前读(如
SELECT COUNT(*) FROM t WHERE cond FOR UPDATE),提前建立锁范围,后续同条件操作就不会跳出该视图
最易被忽略的一点:即使你写了 FOR UPDATE,如果 MySQL 执行计划没走索引,或者你用的是 UNIQUE KEY 等值查询,间隙锁依然不生效——得看 EXPLAIN 输出的 key 和 type 字段,而不是只信 SQL 表面写法。


















