跳转到主内容
极星编程网:以代码为星,赴技术山海!

PHP MySQL 事务隔离级别详解:MVCC 多版本并发控制机制与死锁预防策略

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免费学习笔记(深入) ”; PHP 8.5.5 PHP 8.5.5 是 PHP 8.5 分支的维护更新版本。该版本延续了“小步快跑”的迭代逻辑,通过深度错误修复、底层性能微调以及安全加固,旨在为开发者提供一个更健壮、更高效的运行环境。该版本严格遵守语义化版本规范,不包含破坏性变更。 下载
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 效率;它把隔离性推给开发者判断——什么时候该用快照读,什么时候必须当前读,什么时候得加应用层重试,这些边界往往比语法更难把握。

相关文章