SHOW ENGINE INNODB STATUS\G 的 LATEST DETECTED DEADLOCK 提供最新死锁快照,需重点分析 TRANSACTION、WAITING FOR THIS LOCK 和 HOLDS THE LOCK(S) 三部分,并结合 EXPLAIN 验证加锁行为,避免间隙锁与非确定性分页引发死锁。

直接看 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(谁持着这把锁) -
*** (2) HOLDS THE LOCK(S):下列出该事务当前持有的所有锁,包括表锁、间隙锁、记录锁
不要只扫 SQL 文本——很多死锁不是由 UPDATE 本身触发的,而是前一条 SELECT ... FOR UPDATE 或子查询提前加了间隙锁,UPDATE 只是“踩上去”的最后一环。
用 EXPLAIN 验证 UPDATE 是否真按你设想的顺序加锁
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 - 彻底删掉无业务意义的唯一索引(比如同时建了
email和LOWER(email)的 UNIQUE),减少 gap lock 冲突面
别信“我查的是主键,肯定安全”——主键查不到,照样加间隙锁。
批量 UPDATE 必须用主键范围切片,别信 LIMIT 分页
LIMIT 在 UPDATE 中没有幂等分片能力。当其他事务正在并发 INSERT 或 DELETE 时,“第 3 批 1000 条”可能和上一批重叠,也可能跳过某些行。表面没报错,但业务逻辑已错乱。
正确做法是基于主键(或唯一索引)做确定性区间切片:
- 写成
UPDATE t SET status = 'done' WHERE id BETWEEN 5001 AND 6000 AND status = 'pending' - 确保
id是主键或有唯一索引,避免锁扩大到整张表 - 区间大小要权衡:太小(如每次 10 行)事务开销大;太大(如每次 5 万行)单次锁持有太久,阻塞面广
- 每批执行后显式
COMMIT,释放锁,缩短事务窗口
范围切片看似笨重,却是唯一能规避因并发写入导致的加锁顺序漂移的方法。

















