登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  数据库 >  MySQL

MySQL 死锁日志怎么还原现场:LATEST DETECTED DEADLOCK、锁顺序与最小复现

来源:17golang原创

时间:2026-08-25 12:11:34 480浏览 收藏

线上出现 ERROR 1213 (40001): Deadlock found when trying to get lock 时,单看这一行只能知道事务被回滚了,无法解释是谁先拿了哪把锁。真正有价值的现场通常在 InnoDB 的 LATEST DETECTED DEADLOCK 记录里:它会列出两个事务持有的锁、正在等待的锁,以及最后被回滚的事务。先把这段记录还原成“事务—资源—等待”的闭环,再决定是调整访问顺序、缩短事务,还是只在应用层重试。

要点速览

  • SHOW ENGINE INNODB STATUS 的死锁段落要成对阅读,不能只看最后一行。
  • 持有锁和等待锁拼成环,才是死锁;单向等待通常只是锁竞争。
  • 用两条事务按相反顺序更新 row A、row B,可以稳定复现最小问题。
  • 修复优先统一资源访问顺序,再缩短事务范围,最后为可重试错误设计退避。

先把死锁日志读成一张关系表

直接在复现环境或者故障发生的窗口期执行对应操作:

SHOW ENGINE INNODB STATUS\G

输出中的 LATEST DETECTED DEADLOCK 通常包含两个 TRANSACTION 段。每段先描述当前事务已经持有什么,再描述它正在等待什么。不要把 “WAITING FOR THIS LOCK” 误读成事务已经拿到的锁;还原时应把它拆成四列:

字段要确认的信息对应现场含义
TRANSACTION对应哪个连接/线程快速定位背后的业务请求或后台任务
HOLDS THE LOCK(S)它已经占住了什么资源梳理它阻塞其他请求的资源范围
WAITING FOR THIS LOCK它还想拿到什么锁指向另一个事务当前持有的资源
WE ROLL BACK TRANSACTION谁被选中回滚明确本次死锁的直接处理结果
MySQL InnoDB LATEST DETECTED DEADLOCK 日志中两个事务互相持有并等待行锁的还原关系

实际排查的时候先记下事务编号、线程 ID、表名和索引名,再回到应用日志对应时间窗口里找对应的请求参数。死锁日志里出现相同表名不代表锁冲突已经成立,必须确认一个事务在等的记录,刚好是另一个事务已经持有的资源。

什么时候是死锁,什么时候只是锁等待

普通锁等待只有单向指向:T1 等 T2 释放锁就可以继续。死锁则至少形成一个闭环:T1 持有 row A、等待 row B;T2 持有 row B、等待 row A。InnoDB 检测到这个环之后会选一个事务回滚,让另一个事务可以正常提交。

你可以把日志内容直接整理写在便签或者工单里,对应画出两条事务的资源持有和等待链路:

T1 holds A, waits B
T2 holds B, waits A
=> T1 -> T2 -> T1

如果只能写出 T1 -> T2,那更像是普通锁竞争,应该继续看事务是否长时间未提交、是否被慢查询或网络调用拖住。这个区别会直接影响处理方式:等待问题先缩短占锁时间,闭环问题还要打破访问顺序。

用相反锁顺序构造最小复现

先准备一张只有两行的测试表,同时开两个独立的数据库会话,不要用同一个连接:

CREATE TABLE deadlock_demo (
  id BIGINT PRIMARY KEY,
  balance INT NOT NULL
) ENGINE=InnoDB;

INSERT INTO deadlock_demo (id, balance)
VALUES (1, 100), (2, 100);

会话 A 先锁住 ID 为 1 的行,之后请求 ID 为 2 的行时会进入等待:

START TRANSACTION;
UPDATE deadlock_demo SET balance = balance + 10 WHERE id = 1;
-- 暂停,等待会话 B 先锁住 id=2
UPDATE deadlock_demo SET balance = balance - 10 WHERE id = 2;

会话 B 反过来先锁住 ID 为 2 的行,再请求 ID 为 1 的行,就刚好拼成死锁环:

START TRANSACTION;
UPDATE deadlock_demo SET balance = balance + 20 WHERE id = 2;
UPDATE deadlock_demo SET balance = balance - 20 WHERE id = 1;
MySQL 两个事务按 T1 A到B 与 T2 B到A 的相反锁顺序形成死锁并统一为 A到B 的修复

第二个 UPDATE 会让两个会话互相等待,最终一个连接收到 1213。复现时不要把两个 SQL 放在同一连接,也不要让客户端自动提交;否则锁不会跨语句保持,现场就不稳定。

修复优先级:先统一顺序,再控制事务长度

最容易验证生效的修复方案,就是让同一类业务流程始终按固定顺序访问资源:比如转账场景里,不管资金往哪个方向走,都先锁 ID 更小的账户,再锁 ID 更大的账户。这样两个事务最多只会排队等锁,不会因为加锁顺序相反而形成环。

  • 统一访问顺序:批量更新、转账、库存扣减这类场景,都先把要操作的资源 ID 排序之后再依次加锁。
  • 缩短事务时长:不要在持有行锁的过程中调用远程接口、等待用户输入或者执行大批量计算。
  • 缩小锁范围:保证更新语句的索引可以精准命中目标行,避免因为查询条件不全锁住远大于预期的记录范围。
  • 应用端重试:只对明确抛出的 1213/40001 错误做有限次数重试,每次重试都重新开启完整事务,搭配合理的退避时长,不能复用已经失败的连接状态继续执行。

重试只是最后一道兜底恢复措施,不能替代前期的锁顺序设计。如果每次事务还是以相反顺序加锁,重试只会把锁冲突推迟到下一轮,没法从根源解决问题。

用验收清单确认修复没有换来新问题

修复完成后至少做三组验证:相同并发压力下不再出现锁等待闭环;事务提交前没有额外的跨网络远程调用等待;出现普通锁等待时,等待时长和持锁时长都在预设的可接受范围内。测试记录里要保留SQL执行顺序、并发连接数、死锁回滚次数、平均事务耗时和最长锁等待时间这些信息。

如果线上已经发生过死锁,建议同时存一份脱敏后的 InnoDB 状态片段和对应时间段的请求日志。后续再出现同类问题时,先比对资源访问顺序是否符合之前的修复要求,再判断是不是新上线的索引或者批量任务引入了其他的加锁路径。

常见问题

死锁一定说明 MySQL 配置错了吗?

不一定。绝大多数死锁都是不同业务事务以相反顺序访问同一组行造成的,数据库自动检测并回滚是自带的正常保护机制,先优先检查锁访问顺序和事务边界配置。

为什么只看到一个事务被回滚?

InnoDB 会选择修改行数更少、回滚代价更小的事务来回滚,打破等待闭环,剩下另一个事务继续执行。所以应用层必须把 1213 错误当成一次完整的事务失败来处理,不能认为只有部分操作没执行。

捕获 1213 后能不能只重试最后一条 SQL?

不能直接在当前会话继续往下执行。触发死锁的原事务已经被完整回滚,应该重新开启新事务,重新读取需要的最新数据,按修复后的加锁顺序执行全部业务步骤。

SHOW ENGINE INNODB STATUS 的死锁记录会保留多久?

这个字段展示的只是最近一次被检测到的死锁。如果要做长期问题分析,应该把这段关键输出接入故障记录或者监控采集链路,避免下一次死锁日志把上一次的现场直接覆盖掉。

还原 MySQL 死锁的核心不是死背错误码,而是把日志里每个事务的「已持有资源」和「正在等待资源」梳理成有向图。只要能明确定位出等待闭环,就可以通过统一锁顺序、短事务设计、可控重试这几个方向逐项验证修复效果。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>