死锁根因是并发写入路径重叠与事务执行顺序冲突,非服务中断本身;ThinkPHP 5.1+ 必须用 Db::transaction() 闭包写法,startTrans() 因连接惰性初始化而失效,混用模型与原生查询、读写分离、隐式提交、加锁顺序不一致、WHERE 未命中索引、事务中耗时操作、隔离级别误配等均加剧死锁风险。

死锁不是服务中断的根因,而是并发写入路径重叠 + 事务执行顺序偶然冲突的结果;ThinkPHP 5.1+ 必须用 Db::transaction() 闭包写法,否则事务根本不会生效。
为什么 startTrans() 在 ThinkPHP 5.1+ 里基本没用
根本问题不在语法,而在连接惰性初始化机制:startTrans() 调用时可能还没真正拿到 PDO 实例,等你执行 User::create() 或 Db::table()->insert() 时,才新建连接——结果 BEGIN 发给 A 连接,INSERT 却跑在 B 连接上。
- 混用模型操作(如
User::save())和原生查询(如Db::query()),底层连接对象很可能不同 - 启用读写分离(
'deploy' => 1)后,手动事务无法跨节点保证原子性 - 典型错误现象:
rollback()执行了但数据照旧写入、commit()报There is no active transaction、日志里完全看不到BEGIN记录
怎么确认真是死锁,而不是其他异常
只有 $e->getCode() === '1213' 才代表 MySQL 死锁;SQLSTATE[40001] 是序列化失败,不是死锁,别混淆。
- PDO 默认不抛异常,必须在数据库配置中显式设
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION - 日志里看到
Deadlock found when trying to get lock但没1213码?说明 PDO 配置漏了,catch根本捕不到 - 在事务中调用
Db::query()或Db::execute()可能触发隐式提交,导致后续语句实际不在事务内——此时报错根本不是死锁,而是逻辑断裂
事务内加锁顺序不一致是死锁高发点
多个事务以不同顺序访问同一组资源,极易形成循环等待。比如事务 A 先更新 users 再更新 orders,事务 B 反过来,就必然卡住。
立即学习“PHP免费学习笔记(深入)”;
- ThinkPHP 中批量操作如
WHERE id IN (1,3,2)的实际加锁顺序由 MySQL B+ 树扫描决定,建议提前sort()ID 数组或加ORDER BY id强制顺序 - UPDATE/SELECT FOR UPDATE 的 WHERE 条件必须命中索引,否则全表扫描会锁住所有行;避免
WHERE DATE(create_time) = '2024-01-01'这类函数运算,改用create_time BETWEEN '2024-01-01 00:00:00' AND '2024-01-01 23:59:59' - 不要在事务中做 HTTP 请求、文件读写、慢查询等耗时操作,锁持有时间从毫秒拉长到秒级,冲突窗口陡增
临时调隔离级别要精准,不能全局改
MySQL 默认 REPEATABLE READ 在某些更新场景下比 READ COMMITTED 更容易引发间隙锁冲突;但全局改成 READ COMMITTED 会影响一致性读行为,比如幻读变多、部分业务逻辑出错。
- 只在明确需要降低锁强度的写操作里临时调整:
Db::transaction(fn() => { ... }, 'read committed'),注意该配置仅对当前连接生效 - ThinkPHP 6.3+ 支持该写法,但需确保 MySQL 版本 ≥5.7 且未启用
innodb_locks_unsafe_for_binlog - 别在
database.php里全局设'default_strict' => false或改'deploy' => 0来“绕过”隔离控制,这会让底层驱动忽略你传的参数
真正难处理的是那些没走索引的 UPDATE、SELECT FOR UPDATE,以及事务中混用模型与原生查询导致的连接错位——这些细节一旦漏掉,重试逻辑再完善也压不住死锁频率。



















