mysql的rr隔离级别并未彻底解决幻读,仅对特定快照读“看似”防住;混用快照读与当前读、无索引查询、等值条件不触发间隙锁等场景均会导致幻读。

MySQL 的 RR 隔离级别并没有彻底解决幻读——这是最关键的事实。它只在特定条件下、对特定类型的读操作“看起来”防住了幻读,但只要业务逻辑稍一复杂,幻读就可能暴露。
下面从实际开发中最常踩坑的几个点切入,说清楚「怎么做」「为什么这样」和「哪里会翻车」。
RR 下快照读不等于防幻读,只是固定了 ReadView 时间点
普通 SELECT 在 RR 下是快照读:事务第一次执行 SELECT 时生成一个 ReadView,后续所有无锁 SELECT 都复用它。哪怕其他事务插入并提交了新行,只要其 DB_TRX_ID 大于当前事务的 up_limit_id,就不会被看到。
常见错误现象:SELECT * FROM users WHERE age > 25 执行两次结果一样,就以为“幻读被解决了”。其实这只是 MVCC 的副作用,不是隔离级别本身保证了范围稳定性。
- 快照读不加锁,也不阻塞插入,更不感知新提交的行
- 如果事务里混入了
UPDATE或SELECT ... FOR UPDATE,InnoDB 会立即切换为当前读,MVCC 失效,ReadView不再复用 - 没有索引的
WHERE条件会导致全表扫描,快照读性能差,但幻读反而“不明显”——因为根本查不到新行(新行还没被 MVCC 版本链纳入)
当前读才真正触发间隙锁,但只在索引范围查询中生效
SELECT ... FOR UPDATE、UPDATE、DELETE 这些语句属于当前读,InnoDB 必须访问最新已提交数据,并尝试加 next-key lock(记录锁 + 间隙锁)。但这个机制有严格前提:
- 必须走索引:如果
WHERE b = 1中b没有索引,InnoDB 只能全表扫描,间隙锁退化为行锁甚至失效 - 必须是范围条件:
WHERE id > 100会锁住(100, +∞);而WHERE id = 100(主键等值)只加记录锁,不锁间隙,别人仍可在 99 和 101 之间插入 - 唯一索引上的等值查询(如
WHERE a = 7,a是UNIQUE KEY)也可能退化为记录锁,无法阻止相同a值的新插入(除非配合INSERT ... ON DUPLICATE KEY UPDATE)
混用快照读和当前读是幻读高发场景
这是生产环境最典型的问题来源:事务内先做一次普通 SELECT 拿到旧视图,再执行 UPDATE ... WHERE x > 10(当前读),最后又用 SELECT 验证——三次结果可能完全不同。
示例流程:
- 事务 A:
SELECT * FROM t WHERE id > 10→ 返回 5 行 - 事务 B:插入
id = 15并提交 - 事务 A:
UPDATE t SET c = 1 WHERE id > 10→ 实际影响 6 行(当前读穿透 MVCC) - 事务 A:
SELECT * FROM t WHERE id > 10→ 仍只返回 5 行(快照读复用旧ReadView) - 事务 A:
SELECT * FROM t WHERE id > 10 FOR UPDATE→ 突然看到第 6 行
这不是 bug,是设计使然:快照读和当前读服务于不同语义,RR 并不承诺二者结果一致。
想真正规避幻读,得靠显式控制,而不是依赖 RR 默认行为
别指望 RR 自动兜底。可靠做法是主动收敛读取模式:
- 所有需要强一致性的范围查询,统一使用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE,确保每次都走当前读 +next-key lock - 避免在同一个事务里交替使用无锁
SELECT和加锁语句;要么全快照读(接受幻读风险),要么全当前读(用锁覆盖) - 在事务开始后立刻执行一次关键范围的
SELECT ... FOR UPDATE,提前锁定间隙,后续同条件操作就不会跳出该锁区间 - 慎用
SERIALIZABLE:它确实能把所有SELECT转为加锁读,但并发性能断崖式下跌,且死锁概率飙升,多数业务扛不住
最易被忽略的一点:间隙锁只在当前读中起作用,而 INSERT 本身不加间隙锁——它是否被阻塞,完全取决于插入位置前后索引项上有没有已被持有的间隙锁。没索引?那基本等于不设防。











