RR隔离级别下幻读未被完全解决:快照读靠MVCC避免幻读,但当前读需next-key lock配合索引才能防止,无索引时仍可能幻读。

RR 隔离级别下幻读真的被解决了?
MySQL InnoDB 在 REPEATABLE READ(RR)隔离级别下,**对快照读(普通 SELECT)确实避免了幻读,但对当前读(SELECT ... FOR UPDATE、UPDATE、DELETE)只在加锁范围内防住——漏掉的间隙仍可能出问题。** 很多人误以为“RR 就不幻读”,结果在线上看到两次 SELECT ... FOR UPDATE 返回行数不同,才意识到:不是没幻读,是没锁对范围。
快照读靠 MVCC,但别指望它管写操作
MVCC 通过维护每行的 trx_id(创建版本)和 roll_ptr(回滚指针),配合事务启动时生成的 ReadView,让普通 SELECT 只看到“该事务开始前已提交”的数据快照。所以即使其他事务插入新行,你查不到——这叫“幻读被屏蔽”。
- ✅ 适用场景:报表统计、后台只读查询、不需要实时一致性的读取
- ❌ 不适用:你要基于这次查询结果做
INSERT或UPDATE,比如“查有没有同名用户,没有就注册”——这时 MVCC 拦不住别人插进来 - ⚠️ 坑点:
SELECT不加锁 ≠ 安全;如果业务逻辑依赖“查无此记录 → 插入”,必须改用当前读+锁,否则必然竞态
当前读必须用 next-key lock,但索引是前提
要真正阻止幻读,得让 SELECT ... FOR UPDATE 或 UPDATE ... WHERE 锁住“值本身 + 值之间的间隙”。InnoDB 实现这个靠的是 next-key lock(记录锁 + 间隙锁),但它**只在有索引的列上生效**。如果 WHERE 条件走的是全表扫描,InnoDB 可能退化为锁表或锁大量无关间隙,性能崩盘。
SELECT * FROM user WHERE name = '张三' FOR UPDATE;
- ✅ 有效前提:字段
name有索引(哪怕非唯一);否则锁不住间隙,别人仍可在“张三”前后插入新记录 - ✅ 更稳妥写法:用主键或唯一索引精确匹配,如
WHERE id = 123 FOR UPDATE,此时只加记录锁,开销最小 - ⚠️ 常见错误:
WHERE status = 0却没给status建索引 → 锁全表 or 大量间隙 → 并发下降、死锁风险飙升
什么时候该切到 Serializable?真要慎用
SERIALIZABLE 是唯一能 100% 消灭幻读的隔离级别,原理是自动把所有 SELECT 转成 SELECT ... LOCK IN SHARE MODE。但它代价巨大:
- ❌ 写操作会被读阻塞(反之亦然),TPS 断崖式下跌
- ❌ 在高并发订单、库存类场景中,极易触发锁等待超时(
Lock wait timeout exceeded) - ✅ 仅建议:低频、强一致性要求的批处理任务,比如财务日结核对
绝大多数业务,更推荐“RR + 精准索引 + 当前读显式加锁”,而不是一刀切升隔离级别。幻读不是玄学,是锁没锁对地方——查哪段数据,就得锁住那段数据的“存在区间”。










