db::transaction()未抛异常因pdo默认非异常模式且混用事务管理;死锁错误码为1213,需检查$e->errorinfo[1];多表更新须统一顺序,mysql应开启innodb_print_all_deadlocks监控。

死锁发生时 Db::transaction() 为什么没抛异常
ThinkPHP 默认的 Db::transaction() 在 MySQL 死锁(Deadlock found when trying to get lock)时,不一定立即抛出异常——它取决于底层 PDO 的 ATTR_ERRMODE 设置和事务是否已显式开启。如果用的是手动 startTrans() + commit()/rollback() 流程,而中间某条 SQL 触发死锁,PDO 可能只在下一次执行时才报错,导致事务状态错乱。
实操建议:
- 确保数据库连接配置中启用异常模式:
'params' => [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION] - 避免混用
Db::transaction()(自动管理)和startTrans()(手动管理),同一段逻辑只选一种 - 死锁是瞬态错误,捕获后应重试,而非直接失败;ThinkPHP 本身不内置重试逻辑,需自行封装
如何捕获并识别 MySQL 死锁错误
MySQL 死锁对应的错误码是 1213,错误信息固定为 Deadlock found when trying to get lock。但注意:PDO 抛出的 PDOException 中,$e->getCode() 返回的是字符串(如 "HY000"),真正数字码在 $e->errorInfo[1] 里。
实操建议:
- 用
try...catch包裹事务块,检查$e->errorInfo[1] === 1213,而不是依赖$e->getCode() - 不要只匹配错误信息字符串(如
strpos($e->getMessage(), 'Deadlock')),因为不同 MySQL 版本或语言环境可能返回翻译后文本 - 日志中务必记录
$e->errorInfo全量三元组,方便后续分析冲突的 SQL 和事务 ID
事务内多表更新顺序不一致是死锁主因
ThinkPHP 本身不干预 SQL 执行顺序,但业务代码中如果 A 接口先更新 user 再更新 order,B 接口反过来,高并发下极易触发死锁。框架不会帮你做语句排序或锁粒度优化。
实操建议:
- 所有涉及多个表的事务,强制约定更新顺序(如始终按
user → order → log顺序操作) - 避免在事务中调用外部 HTTP 或耗时服务,防止锁持有时间过长
- 用
SELECT ... FOR UPDATE时,确保 WHERE 条件命中索引,否则会升级为表锁,放大死锁概率
监控死锁不能只靠应用层捕获
应用层只能捕获自己发起的死锁,但 MySQL 自身每发生一次死锁,都会写入错误日志(innodb_print_all_deadlocks = ON),并记录完整事务堆栈。仅靠 PHP 捕获,会漏掉被自动回滚的“静默死锁”。
实操建议:
- 在 MySQL 配置中打开
innodb_print_all_deadlocks = ON,并将错误日志路径纳入日志收集系统 - 定期解析 slow-log 或 error-log,提取含
*** (1) TRANSACTION和*** (2) HOLDS THE LOCK(S)的段落 - ThinkPHP 的
Db::listen()只能监听 SQL 执行,无法感知锁等待或死锁事件,别指望它替代数据库原生监控
死锁不是代码写错,而是并发路径的必然产物;能复现的死锁往往说明业务流程存在可预测的竞争点,重点不在“怎么 catch”,而在“哪几行 SQL 总是一起出现”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











