<p>最快定位最近一次死锁现场的方法是立即执行SHOW ENGINE INNODB STATUS\G,重点查看LATEST DETECTED DEADLOCK区块,其中 (1)和 (2)事务的SQL语句、HOLDS THE LOCK(S)与WAITING FOR THIS LOCK TO BE GRANTED对比可直接揭示循环等待根因。</p>

MySQL 生产环境出现 Deadlock found when trying to get lock 错误,不用 panic —— InnoDB 已经自动回滚了其中一个事务,业务链路本身没卡死;但必须立刻查日志定位根因,否则同一段逻辑会反复触发死锁。
怎么快速定位最近一次死锁的完整现场?
执行 SHOW ENGINE INNODB STATUS\G 是唯一可靠入口,别依赖错误日志(默认只记最后一次,且不带 SQL 上下文)。
- 输出中必须找到
LATEST DETECTED DEADLOCK段落,它包含两个事务的完整加锁路径:各自执行的UPDATE/SELECT FOR UPDATE语句、持有的锁类型(X行锁 /GAP间隙锁)、等待的索引记录 - 重点关注
*** (1)和*** (2)两段里WAITING FOR THIS LOCK TO BE GRANTED和HOLDS THE LOCK(S)的对比——这就是循环等待的证据 - 如果看不到该段落,说明死锁已过去,或你连的是从库(
INNODB STATUS不在从库生效),要切到主库查
为什么死锁总在相同接口/SQL 上重复发生?
90% 的复现死锁,根源是事务访问顺序不一致或索引失效,不是并发偶然事件。
- 检查涉及的 SQL 是否都走了索引:用
EXPLAIN确认type是ref/range,而非ALL或index;key列显示实际使用的索引名 - 对比两个死锁事务的 SQL 执行顺序:比如事务 A 先
UPDATE inventory再UPDATE order,事务 B 反过来,这就是典型交叉锁 - 注意 ORM 自动生成 SQL 的隐患:MyBatis 的
<where>标签内条件顺序可能随参数变化,导致同个方法生成不同执行顺序的 SQL
应用层要不要加重试?加多少次?
加,但仅作为兜底,不能替代根因修复。重试逻辑本身有放大风险的副作用。
- 只对明确的死锁错误码重试:
errno == 1213(MySQL)或SQLSTATE == '40001'(标准 SQL) - 重试次数建议 ≤ 3 次,每次间隔递增(如 50ms → 100ms → 200ms),避免雪崩式重试压垮数据库
- 重试前必须重新生成事务上下文(比如 Spring 的
@Transactional要确保每次重试都是新事务),否则可能复用已部分执行的脏状态 - 记录每次重试的原始 SQL 和参数,用于后续比对是否真因顺序问题导致——如果重试后仍失败,大概率是索引或长事务问题,不是单纯运气差
最容易被忽略的三个深水区
修复时往往只盯着 SQL 和事务,但以下三点常被跳过,却直接决定死锁是否真正收敛:
-
innodb_print_all_deadlocks = ON没开:默认只保留最后一次死锁日志,线上高频场景下根本抓不到现场,必须在my.cnf加上并重启 MySQL - 间隙锁(Gap Lock)被低估:
WHERE price BETWEEN 100 AND 200这类范围查询,在 RR 隔离级别下会锁住区间内所有“可能插入的位置”,两个事务范围重叠就极易死锁,解法是改用等值查询 + 精确索引,或降级为 RC(需评估幻读影响) - 事务里混了非 DB 操作:比如更新完订单后调用支付网关,这期间锁一直持有;必须把外部调用提到事务外,用本地消息表或补偿机制保证最终一致性


















