MySQL的RR隔离级别不能完全解决幻读,根本原因在于快照读依赖MVCC提供一致性视图但不锁间隙,而当前读依赖Next-Key Lock防止插入,两者机制分离且混合使用时会导致同一事务内读取结果不一致。

MySQL 的 RR 隔离级别不能完全解决幻读,根本原因在于它对“快照读”和“当前读”采取了两种完全不同的处理机制——前者靠 MVCC 隔离,后者依赖锁,而幻读恰恰可能在两者混合使用时暴露。
快照读(SELECT)为什么看起来没幻读
在 RR 级别下,普通 SELECT 是快照读:事务启动时生成一个 Read View,之后所有 SELECT 都基于这个时间点的已提交数据版本返回结果。即使其他事务插入并提交了新行,只要没被当前事务的 Read View 看见,就不会出现在结果里。
常见错误现象:你在事务 A 中执行两次 SELECT * FROM users WHERE age > 20,结果一致,就以为“RR 解决了幻读”——其实这只是快照读的副作用,并非隔离级别本身消灭了幻读。
- 快照读不加锁,不阻塞插入,也不感知新提交的行
- 它只保证“读一致性”,不保证“查询范围稳定性”
- 如果业务逻辑只用
SELECT,那确实不会看到幻行;但一旦混入写操作,问题立刻浮现
当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)才是幻读高发区
当语句触发当前读时,InnoDB 必须访问最新已提交数据(不是快照),此时 MVCC 失效,转而依赖锁机制。是否能防住幻读,取决于有没有命中 Next-Key Lock 覆盖的间隙。
典型陷阱:你对 WHERE age > 20 加锁,但 age 列没索引,或只有普通索引且值分布稀疏——InnoDB 可能退化为表锁或锁不住关键间隙,导致其他事务仍能插入满足条件的新行。
-
UPDATE users SET name='x' WHERE age > 20是当前读,会扫描并锁住所有匹配行 + 相邻间隙 - 但如果
age无索引,InnoDB 只能全表扫描,间隙锁失效,插入不受限 - 即使有索引,若查询条件是
WHERE age = 25(等值),间隙锁只锁该值前后间隙;而WHERE age > 25(范围)才会锁右侧无限间隙——这点极易被忽略
快照读 + 当前读混合使用必然暴露幻读
这是最常被低估的场景:事务内先做一次快照读拿到旧结果,再执行当前读更新,最后又用快照读验证——三次读取的数据视图不一致,用户直观感受到“凭空多了一行”。
示例中事务 A:SELECT(看到 2 行)→ 事务 B 插入并提交 → 事务 A 执行 UPDATE ... WHERE age > 20(当前读,影响 3 行)→ 再 SELECT(快照读,仍只看到 2 行)→ 最后 SELECT 前加 FOR UPDATE(当前读,突然看到第 3 行)——这不是 bug,是设计使然。
- 快照读和当前读本就面向不同语义:一个是“我需要历史一致视图”,一个是“我需要最新状态来修改”
- RR 级别不承诺“所有读操作行为统一”,只承诺“快照读可重复”
- 如果你需要全程强一致,必须统一用当前读(如全部加上
FOR UPDATE),或改用SERIALIZABLE
真正容易被忽略的点是:**幻读是否发生,不取决于隔离级别声明,而取决于你每条 SQL 属于快照读还是当前读,以及当前读是否成功施加了覆盖查询范围的 Next-Key Lock。** 没有索引、查询条件不走索引、使用函数包装列(如 WHERE YEAR(create_time) = 2025)——这些都会让间隙锁失效,幻读风险直接回归 RC 级别。


















