mysql在read committed下必然出现幻读,是设计使然;repeatable read防幻读仅对当前读生效,需配合索引与next-key锁,快照读不防插入。

MySQL 在 READ COMMITTED 隔离级别下必然会出现幻读,这不是 bug,而是该级别的设计行为;真正能稳定防幻读的,是 REPEATABLE READ(RR),但前提是用对了——不是设个级别就完事,得看读法、索引、锁类型。
RR 级别下为什么有时还是出现幻读
根本原因在于:RR 防幻读只对「当前读」生效,而默认的 SELECT 是「快照读」,靠 MVCC 实现,它不加锁,也不拦插入——所以如果你在 RR 下只写普通 SELECT,两次查询结果不同,那不是幻读没被解决,是你根本没触发它的防护机制。
常见误判场景:
- 用
SELECT * FROM users WHERE age > 30查两次,中间别人插入了一条age=35的记录,结果多了一行 → 这是快照读,MVCC 保证你看到的是事务启动时的一致视图,但这个视图不含新插入行,所以“多出来”其实是你第二次查用了新 ReadView?不,RR 下第一次SELECT就生成 ReadView 并复用,因此这种“多出来”往往说明你实际运行在READ COMMITTED级别,或者应用层连接没正确设置隔离级别 - 用
SELECT ... FOR UPDATE或UPDATE做当前读,但查询条件没走索引 → InnoDB 无法加next-key lock,退化为表锁或锁不住间隙,别人仍可插入
必须用 next-key lock 才算真正防住幻读
next-key lock = 记录锁 + 间隙锁,是 InnoDB 在 RR 级别下对范围查询(如 WHERE id > 10)自动启用的锁。它锁住的是「索引记录本身」和「该记录前一个索引值到本记录之间的间隙」,比如索引有 5, 10, 15,SELECT ... WHERE id > 10 FOR UPDATE 会锁住 (10, 15] 和 (15, +∞) 两个区间。
实操要点:
- 查询必须走索引(主键或二级索引),否则无法定位间隙,InnoDB 可能全表扫描并升级为表锁,性能崩,且不一定防住所有插入
- 条件要是范围(
>,, <code>BETWEEN,LIKE 'abc%'),等值查询(=)只加记录锁,不锁间隙,插在前后间隙里仍算幻读 - 显式加锁语句才触发:
SELECT ... LOCK IN SHARE MODE、SELECT ... FOR UPDATE、UPDATE、DELETE
READ COMMITTED 下没法靠锁防幻读
READ COMMITTED(RC)级别下,InnoDB 不用 next-key lock,只对命中的记录加行锁(record lock),间隙完全开放。即使你写 SELECT ... FOR UPDATE,也只能锁住已有行,别人照样能在间隙里 INSERT 新数据。
所以如果你的应用逻辑依赖「两次范围查询结果一致」,又卡在 RC 级别(比如旧项目兼容性要求),不能只靠换 SQL,得改方案:
- 提前用
SELECT ... FOR UPDATE锁住整个范围(但需确保该范围可被索引覆盖,否则可能锁表) - 改用唯一约束+重试:插入前先
SELECT判重,失败则捕获Duplicate entry错误后重试,适合幂等场景 - 业务层加分布式锁(如 Redis),粒度粗、延迟高,仅当数据库层无解时兜底
最常被忽略的一点:幻读是否真的需要被“解决”,取决于你的语义。如果只是报表统计,快照读的结果天然一致;如果是资金扣减、库存预占,就必须当前读+间隙锁——别指望设个 SET TRANSACTION ISOLATION LEVEL REPEATABLE READ 就万事大吉,得看 SQL 怎么写、索引怎么建、锁怎么落。











