死锁是事务间循环等待而非数据库宕机,thinkphp中多因lock(true)或forupdate()加锁顺序不一致引发;需开启innodb_print_all_deadlocks并结合show engine innodb status查看死锁详情,配合thinkphp开启trigger_sql、log_sql等日志定位sql与上下文,核心解决策略是统一按主键升序加锁、确保where条件走索引、高频更新优先用原子操作,并避免事务中混入http/redis等长耗时操作。

死锁不是数据库挂了,而是两个事务互相卡住对方——ThinkPHP 里最常因为 lock(true) 或 forUpdate() 在事务中加锁顺序不一致触发。
查 MySQL 死锁日志:确认是否真有死锁发生
MySQL 默认检测死锁,但默认不记录详情。只靠 PHP 报错 Deadlock found when trying to get lock 不够,得看 InnoDB 实际输出:
- 执行
SHOW VARIABLES LIKE 'innodb_print_all_deadlocks';,返回ON才算开启 - 死锁信息会写进 MySQL 错误日志(路径查
SHOW VARIABLES LIKE 'log_error';),不是慢查询日志 - 临时快速看最近一次:运行
SHOW ENGINE INNODB STATUS\G,翻到LATEST DETECTED DEADLOCK区域 - 注意:该开关只对 InnoDB 表生效,MyISAM 完全不参与死锁检测
看 ThinkPHP 日志:把 SQL 和上下文对上号
ThinkPHP 默认不记录事务内执行的 SQL,死锁发生时你只能看到报错,看不到哪条语句在锁什么。必须主动补全链路:
- 在
config/database.php中打开'trigger_sql' => true(必须开,否则日志为空) - 配合
'log_sql' => true和'log_bind' => true,才能看到绑定参数后的完整 SQL - 关键是在事务开始前打标记日志,比如:
error_log("[TX] stock_deduct, order_id={$id}, uid={$uid}"); - 这样在 MySQL 死锁日志里看到
UPDATE stock SET num = num - 1 WHERE id = 5,就能反推是哪个订单扣减逻辑出的问题
盯住加锁顺序:90% 的死锁源于这个细节
死锁不是“锁太狠”,而是“锁得乱”。两个事务分别操作相同几行数据,但顺序相反,就必然循环等待:
- 事务 A:
SELECT ... WHERE id = 1 FOR UPDATE→SELECT ... WHERE id = 2 FOR UPDATE - 事务 B:
SELECT ... WHERE id = 2 FOR UPDATE→SELECT ... WHERE id = 1 FOR UPDATE - 解决办法只有一个:所有地方统一按主键升序(或其它全局确定顺序)加锁,比如先
WHERE id IN (1,2)再ORDER BY id ASC后遍历 - 避免在事务里用
ORDER BY id DESC LIMIT 1这类无索引排序,可能触发间隙锁扩散,让本不该冲突的范围也锁住
加锁前必须走索引:否则行锁变表锁
lock(true) 看似锁一行,但如果 WHERE 条件没命中索引,InnoDB 会退化为锁整张表——并发一高,所有请求排队等同一把表锁,看着像死锁,其实是性能雪崩:
- 用
EXPLAIN检查你的SELECT ... FOR UPDATE是否走了索引(type字段不能是ALL或index) - 主键、唯一索引、复合索引的最左匹配都行;普通字段没索引?赶紧加
- ThinkPHP 的
where('status', 1)如果status没索引,lock(true)就等于给全表上锁 - 高频更新字段(如库存)优先用原子操作:
UPDATE goods SET stock = stock - 1 WHERE id = ? AND stock >= 1,省去 SELECT 再判断再 UPDATE 的两步加锁
真正难处理的不是死锁本身,而是它藏在并发毛细血管里:同一个事务里混着 HTTP 调用、Redis 操作、文件写入,导致事务时间不可控;或者 Db::transaction(function () { ... }) 里 throw 了异常却没被外层 catch,连接没归还、锁没释放——这些比锁顺序更隐蔽,也更常引发连锁反应。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











