死锁是事务竞争资源的必然现象,非代码错误;需捕获MySQL错误码1213重试,配合索引优化与统一表操作顺序解决。

死锁不是代码写错了,是事务竞争资源的必然现象;ThinkPHP 本身不处理死锁,必须你自己捕获 1213 错误码、重建连接、重试,并配合 MySQL 层面的日志和索引优化才能真正解决。
怎么确认报错真是死锁,不是其他异常
ThinkPHP 抛出的异常里,只有 $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()在事务里执行 SQL,可能触发隐式提交,导致后续语句不在事务中——此时报错根本不是死锁,而是逻辑断裂
怎么写安全的重试逻辑,避免越试越堵
重试不是简单 sleep(50) 后再跑一遍,关键在于每次重试都得用新连接、新开事务,否则第二次 beginTransaction() 会失败。
- 必须调用
$connection->close()(或Db::connect()拿新实例),不能复用旧连接 - 最大重试次数建议 ≤ 3,第 3 次还失败,说明业务逻辑或表操作顺序本身就有问题
- 退避时间用指数增长:50ms → 75ms → 112ms,别固定 sleep,否则高并发下所有请求一起撞锁
- 重试块内所有读写操作必须在
Db::transaction()回调里完成,不能“事务外查数据再传入”,否则脏读+版本错位
为什么开了重试还是频繁死锁,根源在哪
重试只是兜底,真要降频,得查 MySQL 的 SHOW ENGINE INNODB STATUS\G,重点看 LATEST DETECTED DEADLOCK 区块——它会写出哪两个事务、锁了哪些行、等什么锁。
- 确保 MySQL 配置了
innodb_print_all_deadlocks = ON,否则只留最后一次死锁,线上很难定位 - 常见根因:UPDATE/SELECT FOR UPDATE 的 WHERE 条件没走索引(比如用了
DATE(create_time)),导致全表扫描加锁 - 多表更新时顺序不一致:A 事务先
users再orders,B 事务反着来,就必然形成循环等待 - 事务里做了 HTTP 请求、文件写入、慢查询等非 DB 操作,拉长锁持有时间,冲突窗口变大
lock(true) 和 LOCK TABLES 到底该用哪个
99% 场景该用 lock(true)(即 SELECT ... FOR UPDATE),不是 LOCK TABLES;后者是全局表锁,一锁全表,高并发下直接雪崩。
-
lock(true)是 InnoDB 行级锁,WHERE 命中索引时只锁匹配行,commit/rollback 后自动释放 -
LOCK TABLES user WRITE必须手动UNLOCK TABLES,异常未执行就永久锁表 - 只有离线导入、报表统计等低频且无用户请求的场景才考虑锁表,且必须避开业务高峰
真正卡住人的不是重试写法,而是死锁日志看不懂、索引没建对、多表更新顺序没人约定——这些细节不落地,重试写得再漂亮,也只是把失败从“立刻报错”变成“延迟报错”。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











