mysql rr级别不彻底解决幻读,因快照读与当前读混用时(如先普通select后select for update),中间插入数据会导致第二次看到新行;但通过mvcc保障快照读一致性、next-key lock阻塞范围插入,在多数场景下有效防范幻读。

MySQL 的可重复读(REPEATABLE READ)隔离级别本身不“彻底”解决幻读,但 InnoDB 通过组合手段在绝大多数场景下堵住了幻读漏洞——关键不在隔离级别本身,而在它默认启用的 MVCC + next-key lock 机制。
为什么说 RR 级别“理论上防不住幻读”,但实践中往往没问题
标准 SQL 定义中,只要同一事务内两次 SELECT 返回行数不同,就算幻读。而 MySQL 的 REPEATABLE READ 对“快照读”(普通 SELECT)用 MVCC 隔离视图,对“当前读”(SELECT ... FOR UPDATE、UPDATE、DELETE)加 next-key lock。这意味着:
- 快照读看到的是事务启动时的“一致性视图”,其他事务插入的新行根本不可见 → 不会幻读
- 当前读会锁住匹配条件的记录 + 索引间隙(比如
WHERE id > 10锁住 (10, +∞)),新插入落在该范围内的行会被阻塞 → 插不进去,自然不幻读 - 但如果你混合使用:第一次是快照读(没锁),第二次是当前读(加锁),中间别人插入了数据,那第二次确实能看到新行 —— 这就是教科书级幻读,MySQL 官方也承认这种 case 存在
Gap Lock 和 Next-Key Lock 到底锁什么、什么时候生效
Gap lock 锁的是索引之间的“空隙”,不锁记录本身;next-key lock = record lock + gap lock,是 InnoDB 默认的行锁模式(仅在 RR 隔离级别下启用)。它只在以下情况触发:
- 查询条件命中**非唯一索引**或**范围条件**(如
WHERE age BETWEEN 20 AND 30),且该索引上有对应区间 - 查询使用了 **
FOR UPDATE或LOCK IN SHARE MODE** —— 普通SELECT不会触发 - 表上有主键或唯一索引时,等值查询(
WHERE id = 5)只加record lock,不加gap lock;但WHERE id > 5就会锁 (5, +∞) - 如果查询条件无法走索引(全表扫描),InnoDB 可能升级为锁整个聚簇索引,开销极大,应避免
Repeatable Read 下仍出现幻读的典型场景
不是隔离级别失效,而是你没意识到“读”的类型差异。以下操作组合会暴露幻读:
- 事务 A 执行
SELECT * FROM t WHERE c = 10(快照读,无锁)→ 得到 3 行 - 事务 B 插入
INSERT INTO t (c) VALUES (10)并COMMIT - 事务 A 再执行
SELECT * FROM t WHERE c = 10 FOR UPDATE(当前读)→ 现在看到 4 行,且新行被锁定 - 同理,
UPDATE或DELETE前的“预查”若用快照读,后续 DML 就可能作用于意料之外的行
这种现象不会发生在 SERIALIZABLE 级别下(所有 SELECT 都自动转为 LOCK IN SHARE MODE),但代价是并发大幅下降。
真正要“彻底”防幻读,得看你怎么用
没有银弹,只有权衡。如果你的业务逻辑要求绝对一致的读写视图:
- 对关键校验逻辑,统一用当前读:把
SELECT改成SELECT ... FOR UPDATE,哪怕只是查一下是否存在 - 确保查询条件能命中索引,否则
gap lock可能失效或锁过宽 - 避免在 RR 下依赖“先查后插”的乐观逻辑(如“查无此记录则插入”),改用
INSERT ... ON DUPLICATE KEY UPDATE或唯一约束+捕获Duplicate entry错误 -
SERIALIZABLE是终极方案,但高并发写场景下极易锁等待超时,上线前必须压测
最易被忽略的一点:autocommit=1 时每个语句都是独立事务,MVCC 快照和锁都只作用于单条语句 —— 此时谈“事务内幻读”已无意义,得先确认你的事务边界是否真的跨了多条语句。











