直接查看show engine innodb status\g输出中的latest detected deadlock区块即可准确定位死锁根因,它完整呈现两个事务的sql、线程id、持锁与等待锁关系、索引名及锁类型,是唯一实时、精准、零配置的排查入口。

直接看 SHOW ENGINE INNODB STATUS\G 的死锁快照
MySQL 每次检测到死锁,都会把完整现场写进 SHOW ENGINE INNODB STATUS\G 输出的 LATEST DETECTED DEADLOCK 区域。这不是日志归档,而是内存中最新一次的快照,随时可查。
重点盯三块内容:
-
TRANSACTION开头的两段:分别对应被回滚的事务和存活的事务,记下它们的trx_id和trx_mysql_thread_id -
*** (1) WAITING FOR THIS LOCK TO BE GRANTED:下的lock_mode、lock_type、lock_table、lock_index,以及最关键的lock_rec(行记录地址)或lock_trx_id(谁持着这把锁) -
HOLDS THE LOCK(S):下列出该事务当前持有的所有锁,包括表锁、间隙锁、记录锁
不要只扫 SQL 文本——很多死锁不是由 UPDATE 本身触发的,而是前一条 SELECT ... FOR UPDATE 或子查询提前加了间隙锁,UPDATE 只是“踩上去”的最后一环。
用 EXPLAIN FORMAT=TRADITIONAL 验证加锁顺序是否可控
ORDER BY 不等于加锁顺序保障。MySQL 是否按主键升序逐行加锁,完全取决于执行计划是否走索引扫描;如果优化器退化为全表扫描 + Using filesort,那加锁顺序就是不可控的随机流。
实操步骤:
- 对出问题的
UPDATE语句,先跑EXPLAIN FORMAT=TRADITIONAL确认key字段命中的是你预期的索引(比如PRIMARY或idx_status_created_at),而不是NULL - 检查
Extra列不含Using filesort,否则排序发生在锁获取之后,毫无意义 - 复合索引注意最左前缀:有
(status, created_at)索引,WHERE status = 'pending' ORDER BY created_at才可能生效;WHERE created_at > '2026-01-01'就用不上 - 没索引的
WHERE条件,ORDER BY只是安慰剂,还会拖慢性能
警惕 SELECT ... FOR UPDATE 在无记录时加间隙锁
这是高发但极易被忽略的坑:SELECT ... FOR UPDATE 在 RR 隔离级别下,即使 WHERE 条件查不到任何记录,也会对匹配范围加间隙锁(Gap Lock)。多个并发请求同时查同一个不存在的 order_no,就会卡在同一个间隙上,再叠加后续 INSERT 的插入意向锁,瞬间闭环死锁。
替代方案优先级如下:
- 用
INSERT INTO ... ON DUPLICATE KEY UPDATE,前提是order_no有UNIQUE约束——冲突时直接更新,不走间隙锁 - 若必须用
SELECT ... FOR UPDATE,确保WHERE字段有唯一索引,且EXPLAIN显示type = const或ref、rows = 1
批量 UPDATE 加 LIMIT 反而放大死锁风险
LIMIT 在 UPDATE 中不保证幂等分页,尤其在高并发写入场景下,会导致同一批数据被多次处理或漏处理,隐性拉长事务时间、扩大锁竞争窗口。
更稳妥的做法是:
- 改用游标式分批:用
WHERE id > ? ORDER BY id LIMIT N,每次取最大id作为下一批起点 - 每批独立事务提交,避免跨批次持有锁
- 应用层配合幂等重试逻辑,捕获
Deadlock found when trying to get lock(错误码 1213)后指数退避重试 - 避免在事务内混入 HTTP 调用、日志落盘等非 DB 操作——锁会一直挂着,增大交叉等待窗口
真正难的不是定位死锁,而是确认哪条前序语句悄悄加了锁,又没在日志里留下痕迹。线上环境建议开启 innodb_print_all_deadlocks = ON,把每次死锁都写进 error log,别只依赖那一次内存快照。










