死锁日志不能直接指出错误代码行,但能精确定位引发循环等待的两条SQL及事务执行顺序;需通过LATEST DETECTED DEADLOCK块分析事务ID、持有锁、等待锁及最后SQL,并结合INNODB_TRX和索引信息映射业务逻辑。

死锁日志本身不直接告诉你“哪行代码错了”,但能精准定位是哪两条 SQL、在哪个事务顺序下撞出了循环等待——排查的关键不是看日志多全,而是快速把日志里的锁关系映射回业务逻辑。
怎么看 SHOW ENGINE INNODB STATUS 里的死锁现场
执行后重点盯 LATEST DETECTED DEADLOCK 块,它只保留最近一次,但信息足够做归因:
-
*** (1) TRANSACTION和*** (2) TRANSACTION分别对应两个互锁的事务,注意它们的trx id和trx mysql thread id——后者能关联到INNODB_TRX表查出完整事务上下文 -
*** (1) HOLDS THE LOCK(S)显示该事务当前持有哪些锁(比如RECORD LOCKS space id 123 page no 456 n bits 72),结合后面的index `idx_user_id`可反推出它锁了哪张表、哪个索引、大概哪几行 -
*** (1) WAITING FOR THIS LOCK TO BE GRANTED是关键:它写的不是“等什么资源”,而是“等另一个事务释放什么锁”——如果这里写的是gap before rec,说明是间隙锁冲突;如果是record lock且 index 名和上一行一致,大概率是同一行被两个事务先后 UPDATE - 每条事务末尾的
trx query就是触发死锁的最后一句 SQL,但注意:它未必是“第一句”,只是最后卡住的那句;真正加锁起点往往在 BEGIN 后第一条 UPDATE/SELECT ... FOR UPDATE
为什么 innodb_print_all_deadlocks = ON 必须开
默认 MySQL 只记最后一次死锁到错误日志,而生产环境里死锁常批量发生——你看到的“最近一次”,很可能是凌晨三点那个低频异常,而不是白天每分钟都在发生的主路径问题。
- 开启后所有死锁都会追加进
log_error文件,配合grep -i "deadlock" /var/log/mysql/error.log能统计频率和时间分布 - 配置方式分两步:
SET GLOBAL innodb_print_all_deadlocks = ON(立即生效,重启失效)+ 修改my.cnf加innodb_print_all_deadlocks = 1(持久化) - 副作用极小:每条死锁日志约 2–5 KB,远小于慢查询日志开销;但若每秒死锁超 10 次,建议先优化代码而非关它
- 注意路径权限:MySQL 进程必须对
log_error所在目录有写权限,否则静默失败(不会报错,但日志不生成)
从日志反推应用层改法的三个硬动作
日志只能告诉你“谁锁了谁”,不能自动修复。真正降低死锁率要靠代码侧收敛行为:
- 统一 DML 顺序:比如订单服务里所有涉及
orders和inventory表的操作,强制先 UPDATEorders再 UPDATEinventory;哪怕某个分支逻辑上“应该先扣库存”,也包装成存储过程或在应用层加锁协调,避免跨事务顺序不一致 - 缩小事务粒度:把
BEGIN; SELECT ...; 复杂计算; UPDATE ...; INSERT ...; COMMIT;拆成BEGIN; SELECT ...; COMMIT;+ 独立 UPDATE 事务;计算和日志类非数据库操作一律移出事务外 - 避免隐式锁升级:检查死锁日志里出现的
index `PRIMARY`或index `GEN_CLUST_INDEX`——这往往意味着 UPDATE 语句没走索引,触发了全表扫描+表级锁;给 WHERE 条件字段补索引,或改用主键精确查询
最易被忽略的点:死锁日志里显示的 trx query 很可能来自连接池复用的旧连接,其前面还执行过其他未提交语句;所以光看最后一句 SQL 不够,得结合 INNODB_TRX.trx_started 时间戳,往前查该线程 ID 在相同时间段内执行过的所有语句。


















