幻读只发生在当前读(如SELECT...FOR UPDATE),与快照读无关;因行锁不锁间隙,需索引+next-key lock才能防止。

MySQL 出现幻读,不是因为 MVCC 没起作用,而是因为「快照读」和「当前读」在语义上根本不同——你在事务里混用了它们,又没意识到加锁范围的边界在哪里。
幻读只发生在当前读,和快照读无关
很多人一看到“可重复读下还有幻读”,就怀疑 RR 隔离级别失效了。其实不是:RR 下的 SELECT ... WHERE d=5(无锁)是快照读,它压根看不到其他事务新插入的 (1,1,5);但一旦你写成 SELECT ... WHERE d=5 FOR UPDATE,就切换成了当前读,此时 InnoDB 必须返回所有已提交的、满足条件的最新行——包括刚被插入的那条。
- 快照读靠的是事务启动时生成的 read view,对插入、更新、删除都“视而不见”
- 当前读不看 read view,它要真实数据+加锁,所以必须穿透 MVCC 去查最新版本
- 幻读只可能出现在当前读语句中,比如
SELECT ... FOR UPDATE、UPDATE ... WHERE、DELETE ... WHERE
RR 下的当前读为什么锁不住新插入的行
因为行锁(record lock)只锁住已有记录,而新插入的行落在两个已有索引值之间的“间隙”里。比如表里有 d=0 和 d=5 两行,那么 d=5 的“左间隙”是 (0,5),“右间隙”是 (5,+∞)——插入 (1,1,5) 就落在 (0,5) 里,没被任何 record lock 覆盖。
- InnoDB 用 next-key lock(行锁 + 间隙锁)来封住这个区间,但前提是查询能走索引
- 如果
WHERE d=5中的d没索引,InnoDB 只能全表扫描,这时即使加了FOR UPDATE,也只对扫到的匹配行(如 id=5)加 record lock,其余间隙完全敞开 - 所以 session C 插入
(1,1,5)不会阻塞,Q3 就能读到它——这就是幻读的物理根源
RC 和 RR 在快照读/当前读上的关键差异
区别不在“能不能读到新数据”,而在“什么时候决定读哪个版本”:
- 在 RC 下,每次
SELECT(快照读)都会新建 read view,所以第二次查可能看到 session B 刚提交的更新;但当前读(如FOR UPDATE)依然读最新、加锁、可能幻读 - 在 RR 下,事务第一次快照读后就固定了 read view,后续所有快照读都复用它;但当前读仍然无视该 view,每次都查最新已提交版本
- 也就是说:RC 和 RR 的快照读行为不同,但当前读行为完全一致——都读最新、都加锁、都可能遇到幻读
真正能堵住幻读的,只有显式加锁策略
别指望隔离级别自动解决幻读。MySQL 的 RR 不提供“范围级一致性快照”,它只保证单行多次快照读结果不变。要防止幻读,你得让当前读真正锁住整个范围:
- 确保查询条件字段有索引,否则 next-key lock 退化为 record lock
- 用
SELECT ... FOR UPDATE时,尽量覆盖完整范围,例如把WHERE d=5改成WHERE d>=5 AND d(如果 d 是整数),让优化器更倾向使用范围锁 - 极端场景下,直接用
SELECT ... LOCK IN SHARE MODE+ 应用层校验,或改用SERIALIZABLE(但并发性能断崖下跌) - 注意:
INSERT ... SELECT这类语句也会触发当前读,如果子查询没锁住间隙,同样可能漏掉新插入行
最容易被忽略的一点:幻读不是数据错误,而是语义断裂——你以为锁住了“所有 d=5 的行”,实际上只锁住了“此刻存在的 d=5 的行”。这个认知偏差,比锁本身更难调试。


















