幻读只在“当前读”下真实发生;RR隔离级别下普通SELECT是快照读,不产生幻读,而SELECT...FOR UPDATE等当前读语句因加锁机制缺陷(如无索引时仅行锁、不锁间隙)可能导致幻读。

幻读只在“当前读”下真实发生
很多人误以为可重复读(RR)隔离级别下普通 SELECT 也会幻读,其实不会——RR 下的普通查询是快照读,读的是事务启动时的 MVCC 快照,新插入的行根本不可见。真正出现幻读的,一定是用了 SELECT ... FOR UPDATE、SELECT ... LOCK IN SHARE MODE 或 UPDATE/DELETE 这类“当前读”语句。
典型现象:事务 A 执行 SELECT * FROM orders WHERE status = 'pending' FOR UPDATE 得到 5 行;事务 B 插入一条 status = 'pending' 的新订单并提交;事务 A 再次执行相同语句,突然返回 6 行——多出来的那条就是“幻行”。
为什么 RR 隔离级别仍挡不住幻读
关键在于 InnoDB 的加锁机制:RR 级别下,WHERE 条件无索引或索引不生效时,InnoDB 只能走全表扫描,此时仅对**实际命中的记录**加行锁(Record Lock),而对未命中但落在查询范围内的“间隙”(Gap)不加锁——这就给其他事务留下了插入空间。
- 如果
status字段没建索引,WHERE status = 'pending'会锁住所有扫描到的行,但间隙不锁 → 幻读可发生 - 如果
status有索引,且查询能走索引,InnoDB 会升级为Next-Key Lock(行锁 + 间隙锁),覆盖等值查询的前后间隙 → 大概率阻止幻读 - 但如果查询条件是
WHERE status > 'a'这类范围查询,即使有索引,也可能只锁部分间隙,仍存在漏插风险
真正有效的解决方式不是调高隔离级别
用 SERIALIZABLE 能彻底消灭幻读,但代价是所有 SELECT 都隐式加锁,高并发下极易锁等待甚至死锁,线上基本不用。
更务实的做法是结合场景主动控制:
- 对关键业务逻辑(如库存扣减、订单生成),在事务开头就用
SELECT ... FOR UPDATE锁住整个范围,且确保该字段有合适索引 - 插入前先做一次“存在性校验查询”,但必须搭配
FOR UPDATE,否则校验和插入之间仍有窗口 - 用唯一约束(
UNIQUE KEY)替代应用层判断,让数据库直接拦截重复插入(注意:这防的是重复,不防幻读本身) - 业务上接受“最终一致性”的,可改用乐观锁(如版本号字段 +
WHERE version = ?)配合重试
最容易被忽略的坑:d=5 这种无索引条件
看这个经典例子:SELECT * FROM t WHERE d = 5 FOR UPDATE,而 d 字段没有索引。InnoDB 只会对实际满足 d = 5 的行(比如 id=5)加行锁,其余扫描过的行(id=0,10,15…)不加锁,间隙更不锁。这时别人就能在 id=1、id=2 等任意空隙插入 d = 5 的新行,下次当前读就看到幻行了。
这不是 MySQL 的 bug,而是设计取舍:MVCC 解决快照读一致性,锁机制解决当前读一致性,两者分工明确。想靠 RR 一招鲜解决所有并发问题,本身就是对隔离级别的误解。










