<p>最快查最近死锁用SHOW ENGINE INNODB STATUS并定位LATEST DETECTED DEADLOCK区块,关键看TRANSACTION ID、HOLDS/WAITING FOR LOCK及末尾SQL;查历史死锁须开启innodb_print_all_deadlocks=1,日志中以** (1) TRANSACTION:开头,用grep -A 50 -B 5 "RECORD LOCKS.space id"提取。</p>

查 SHOW ENGINE INNODB STATUS 里的 LATEST DETECTED DEADLOCK
死锁发生后,InnoDB 会自动记录最后一次死锁详情,SHOW ENGINE INNODB STATUS 是唯一能立刻拿到完整上下文的命令。重点不是看“谁被回滚”,而是对比两个事务的 query、锁类型(lock_mode X / S)、索引名(index PRIMARY 或 idx_name)和行键值(rec but not gap 或 gap before rec)。
常见误判点:
- 看到
UPDATE ... WHERE id = ?就以为是主键锁——如果id列没走索引(比如传了字符串或隐式类型转换),实际可能触发全表扫描+间隙锁,扩大死锁面 - 忽略
*** (1) HOLDS THE LOCK(S)和*** (2) WAITING FOR THIS LOCK TO BE GRANTED的对应关系,导致搞反谁先抢锁、谁后卡住 - 把
trx_state = 'LOCK WAIT'当成普通慢查询——它本质是锁冲突,不是 SQL 性能问题
比对事务中 SQL 的执行顺序与索引覆盖情况
Spring 声明式事务本身不决定死锁,但它的方法边界会固化 SQL 执行顺序。比如一个 @Transactional 方法里先后执行:
UPDATE orders SET status='paid' WHERE order_id = 1; UPDATE inventory SET stock = stock - 1 WHERE product_id = 100;
另一个事务若按相反顺序更新 inventory 再更新 orders,就极易形成循环等待。
排查时必须确认:
- 两条 UPDATE 是否都命中了索引?用
EXPLAIN验证,尤其注意type是const还是range或ALL - WHERE 条件是否含函数或类型强制转换(如
WHERE product_id = '100'),这会让索引失效,锁范围从单行扩大到间隙 - 事务内是否有非 DB 操作(如 Feign 调用、文件读写)——它不会出现在死锁日志里,但会拉长持有锁的时间,放大冲突概率
检查 Spring 事务是否真被代理、且 autocommit 已关闭
如果 INNODB_TRX 里压根看不到对应事务,或者 trx_autocommit = 1,说明 Spring 事务根本没生效,所谓“死锁”其实是应用层并发写同一条记录引发的锁等待,和事务机制无关。
典型失效场景:
-
@Transactional方法是private或protected—— Spring AOP 无法生成代理,调用走的是原始对象 - 类内
this.updateOrder()直接调用另一个@Transactional方法——绕过代理,事务切面不触发 - 方法所在类没被 Spring 管理(缺
@Service),或手动new XxxService()实例——容器不介入,更无事务控制 - HikariCP 连接池中
connection-test-query或validation-timeout配置不当,导致连接复用时残留旧事务状态
关联 PROCESSLIST 与 jstack 确认线程真实阻塞点
死锁日志只告诉你“哪个 SQL 卡在哪把锁”,但不告诉你“线程为什么卡住”。比如 trx_query 是空、trx_state = 'RUNNING'、trx_started 超 60 秒,大概率是事务方法里调用了外部服务并超时,而数据库连接还握在手里没释放。
此时要同步查:
-
SELECT ID, USER, HOST, DB, COMMAND, TIME, STATE, INFO FROM information_schema.PROCESSLIST WHERE TIME > 30 AND DB = 'your_db'—— 找出长时间 Sleep 却没断开的连接 - 用
PROCESSLIST.ID匹配应用线程 ID,再用jstack <pid>查该线程栈,确认它是否卡在FeignClient、RestTemplate或Thread.sleep()上 - 若发现大量连接
COMMAND = 'Sleep'且TIME持续增长,基本可判定是 Spring 事务边界外抛异常、未正确回滚,或连接池配置(如max-lifetime)与 MySQLwait_timeout不匹配
真正棘手的不是死锁本身,而是事务方法里混入了非 DB 操作——它让锁持有时间脱离 SQL 执行周期,变成不可预测的黑盒。排查时永远先确认“锁是不是真由这条 SQL 拿着”,而不是一上来就改索引或调隔离级别。


















