rr下混用快照读和当前读必然导致幻读,因快照读基于mvcc固定视图而当前读穿透mvcc读最新数据并加next-key lock,两者结果集不一致;next-key lock仅在索引范围查询的当前读中生效。

RR下快照读和当前读混用必然导致幻读
MySQL的RR隔离级别靠MVCC保证快照读的一致性视图,但只要事务里出现SELECT ... FOR UPDATE或UPDATE这类当前读操作,就立刻切换到加锁路径——此时一致性视图失效,直接读最新版本并加Next-Key Lock。如果之前做过一次快照读(比如普通SELECT),之后又做当前读,两次结果集可能不一致,这就是典型的幻读现象。
常见错误现象包括:
- 事务A第一次
SELECT * FROM t WHERE id > 10返回10行; - 事务B插入
id=15的新行并提交; - 事务A再执行
SELECT * FROM t WHERE id > 10 FOR UPDATE,返回11行。
这不是bug,是设计使然:快照读不感知新插入,当前读必须看到并锁定最新数据。
Next-Key Lock只在当前读时生效,且依赖索引条件
Next-Key Lock(记录锁+间隙锁)是InnoDB在RR下防幻读的核心手段,但它**只在当前读语句明确命中索引范围时才被触发**。没有索引、全表扫描、或WHERE条件无法使用索引,间隙锁会退化为行锁甚至不生效。
实操建议:
- 确保
WHERE字段有有效索引,否则SELECT ... FOR UPDATE只会加行锁,不锁间隙; -
SELECT * FROM t WHERE name = 'xxx'若name无索引,则无法阻止其他事务在相同name值附近插入; - 主键等值查询(如
WHERE id = 100)只加记录锁,不加间隙锁,对幻读无防护作用; - 范围查询(如
WHERE id BETWEEN 10 AND 20)才会激活Next-Key Lock,封锁(10,20]区间。
显式加锁才是防幻读的可靠手段
依赖RR默认行为防幻读风险极高,尤其在业务逻辑涉及“先查后更”时。真正可控的方式是主动控制读取模式:
- 所有需要强一致性的范围查询,统一改用
SELECT ... FOR UPDATE或SELECT ... LOCK IN SHARE MODE; - 避免在同一个事务中混用普通
SELECT和FOR UPDATE——要么全快照读(接受幻读可能性),要么全当前读(用锁兜底); - 如果业务允许,把事务起始点设为
BEGIN后立即执行一次当前读,提前建立锁范围,后续同条件查询就不会跳出该视图。
性能影响明显:Next-Key Lock扩大了锁粒度,高并发插入场景下容易引发死锁或锁等待超时。
Serializable不是银弹,它只是把问题推给应用层
把隔离级别升到SERIALIZABLE确实能全局禁用快照读,所有SELECT自动转为SELECT ... LOCK IN SHARE MODE,从而彻底规避幻读。但代价是并发吞吐骤降,且仍需注意:
- 它不能解决写偏斜(write skew)类问题,比如两个事务分别更新不同行但逻辑上互斥;
- DDL操作(如
ALTER TABLE)可能被长时间阻塞; - 应用层若没处理好锁等待异常(如
Lock wait timeout exceeded),反而更容易出错。
真正容易被忽略的是:幻读是否真的影响业务?很多所谓“幻读”只是开发误判——比如前端分页查总数+查列表,两次快照读本就不保证强一致,这时加锁反而引入不必要的复杂度。











