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