REPEATABLE READ下快照读基于事务启动时的ReadView,故查不到新插入记录;但当前读(如SELECT...FOR UPDATE)会读最新数据并可能因Next-Key Lock间隙冲突而看到新行,导致两次结果不一致,即幻读。

READ COMMITTED 和 REPEATABLE READ 是 MySQL InnoDB 实际可用的两个 MVCC 隔离级别;其他两个(READ UNCOMMITTED、SERIALIZABLE)不走 MVCC 路线,前者直接读最新行,后者全表/行加锁。
REPEATABLE READ 下为什么“查不到新插入的记录”,却仍算幻读?
因为 REPEATABLE READ 的快照读(普通 SELECT)基于事务启动时生成的 ReadView,该视图固定了可见事务 ID 范围(min_id~max_id),后续插入的记录若由新事务提交,其 trx_id 大于当前 ReadView.max_id,自然不可见——这看起来像“没幻读”。
但问题出在当前读:
- 当你执行
SELECT ... FOR UPDATE或UPDATE WHERE ...时,InnoDB 必须读最新数据,并用Next-Key Lock锁住索引区间; - 如果另一个事务在间隙中插入新行并提交,而你紧接着又执行相同条件的
SELECT ... FOR UPDATE,就会看到那条“新行”——两次当前读结果不一致,即幻读。
所以:
- 快照读靠
ReadView隔离,不解决幻读; - 幻读靠
Next-Key Lock抑制,只在当前读场景生效; -
REPEATABLE READ的“可重复”仅对快照读成立,不是绝对隔离。
PHP 中设置隔离级别后,SELECT 是否自动走 MVCC?
是,但仅限于纯读操作且满足以下全部条件:
- 使用 InnoDB 存储引擎;
- 连接未显式开启
FOR UPDATE或LOCK IN SHARE MODE; - 隔离级别为
READ COMMITTED或REPEATABLE READ; - 没有触发隐式当前读(例如通过唯一索引更新前先查主键,可能触发一致性检查)。
常见陷阱:
立即学习“PHP免费学习笔记(深入)”;
-
UPDATE ... WHERE id = ?执行前,InnoDB 会先做一次当前读来定位记录,即使你没写SELECT; -
INSERT ... ON DUPLICATE KEY UPDATE中的冲突检测也属于当前读; - Laravel/Eloquent 的
firstOrCreate()底层含SELECT+INSERT,若并发高,SELECT是快照读,但INSERT可能因间隙锁冲突报死锁。
死锁不是“锁太多”,而是“锁顺序不一致”
InnoDB 死锁典型链路:
- 事务 A 先锁住
id=1(聚簇索引),再尝试锁id=2; - 事务 B 同时先锁住
id=2,再尝试锁id=1; - 双方等待对方释放,形成环路。
关键点:
- 死锁检测开销小,但回滚代价大(尤其已执行大量写操作后);
-
REPEATABLE READ下因Next-Key Lock覆盖间隙,比READ COMMITTED更易出现死锁; - 唯一可靠预防方式是所有事务按相同顺序访问行:比如总是按
id ASC更新,或统一用SELECT ... ORDER BY id FOR UPDATE预占锁; - 不要依赖“先
SELECT再UPDATE”来避免死锁——两次网络往返间,其他事务可能已插队修改。
MVCC 不是银弹。它让读不阻塞写,但没消除锁竞争;它用空间换时间存版本,但 undo log 膨胀会影响 purge 效率;它把隔离性推给开发者判断——什么时候该用快照读,什么时候必须当前读,什么时候得加应用层重试,这些边界往往比语法更难把握。



















