MySQL 事务死锁日志对应索引与访问顺序的排查
来源:17golang原创
时间:2026-09-29 04:30:01 401浏览 收藏
应用收到 MySQL 1213 死锁错误后,正确做法不是先把重试次数调大,而是把 LATEST DETECTED DEADLOCK 里的两个事务、持有锁、等待锁和索引名还原成等待环,再用 EXPLAIN、SHOW INDEX 和业务调用链确认:究竟是两个入口的访问顺序相反,还是索引不合适导致锁定范围超过预期。
一条可执行的排查链路是:记录同一观察窗口的死锁基线,读取最近一次死锁,按事务拆出“已持有”和“正在等待”,把日志中的索引名对应到 SQL 访问路径,最后通过统一加锁顺序、缩小扫描范围和事务级有限重试降低复发。
SHOW ENGINE INNODB STATUS适合查看最近一次 InnoDB 死锁,不是历史死锁报表。- 日志里的索引名只是定位入口,还要结合
SHOW INDEX、EXPLAIN和 SQL 条件判断实际访问范围。 - InnoDB 会选择一个事务回滚;应用应从完整事务边界重试,而不是只重放失败的那条 SQL。
先把死锁变成可比较的基线
先确定固定观察窗口,例如同一业务高峰的 30 分钟,并把死锁发生时间与接口、任务、租户或订单号关联起来。至少记录死锁次数、受影响业务入口、事务 P95 耗时、重试成功率和关键 SQL 的执行计划。没有这组基线,即使改动后错误暂时消失,也很难判断是修复生效还是流量变化。
| 指标 | 改动前 | 改动后 | 用途 |
|---|---|---|---|
| 观察窗口 | 待填写 | 保持相同口径 | 避免错峰数据不可比 |
| 死锁次数 | 待填写 | 待填写 | 判断等待环是否减少 |
| 事务 P95 耗时 | 待填写 | 待填写 | 观察锁等待和重试代价 |
| 重试成功率 | 待填写 | 待填写 | 确认恢复策略是否有效 |
| 扫描行数与所用索引 | 待填写 | 待填写 | 确认锁定范围是否收敛 |
出现问题后先保留应用错误日志中的事务标识、SQL 参数和时间,再查看 InnoDB 最近一次死锁:
-- 查看最近一次 InnoDB 死锁及事务、锁记录信息。 SHOW ENGINE INNODB STATUS;
如果死锁间歇出现且最近一次记录容易被覆盖,可以在受控环境中临时开启全部死锁记录。MySQL 8.4 中 innodb_print_all_deadlocks 默认关闭,开启后会把所有 InnoDB 用户事务死锁写入服务端错误日志。它可能增加日志量并暴露 SQL 上下文,需要具备相应权限,并在采样结束后关闭。
-- 临时把所有 InnoDB 死锁写入 MySQL 错误日志。 SET GLOBAL innodb_print_all_deadlocks = ON; -- 采样结束后关闭,避免长期增加日志量。 SET GLOBAL innodb_print_all_deadlocks = OFF;
从死锁日志还原等待环
在 LATEST DETECTED DEADLOCK 段落中,分别找到两个 TRANSACTION 区块。对每个事务都抄出四项:正在执行的 SQL、已经持有的锁、正在等待的锁,以及锁对应的表和索引。不要只盯着被回滚的事务,因为它只是 InnoDB 选出的受害者,并不等于业务根因一定在这一侧。
接着把信息写成两个静态关系:事务 A 持有索引 X 上的记录锁、等待索引 Y;事务 B 持有索引 Y、等待索引 X。若日志出现 gap、next-key 或范围锁信息,还要把 SQL 的范围条件一并记下。这样才能区分“同一组记录的反向访问”与“范围扫描交叉”这两类问题。

例如日志显示事务 A 已锁住账户记录、正在等待订单记录,而事务 B 已锁住订单记录、正在等待账户记录,这个闭环比“哪条 UPDATE 报错”更有诊断价值。真正需要修改的是两条业务路径共同的锁顺序,而不是只在受害 SQL 外面加一个无限重试循环。
把索引名对应到 SQL 访问路径
死锁日志给出的索引名需要回到表结构中确认列顺序、唯一性和基数,再检查 SQL 是否真的使用该索引。下面用账户表和订单表演示最小检查集合:
-- 核对死锁日志中的索引名、列顺序和唯一性。 SHOW INDEX FROM accounts; SHOW INDEX FROM orders; -- 检查更新条件会选择哪个索引以及估算扫描范围。 EXPLAIN UPDATE orders SET status = 'paid' WHERE tenant_id = 42 AND order_no = 'A1001';
重点查看 possible_keys、key、rows 和 filtered。如果业务条件是 tenant_id + order_no,但执行计划只能使用单列索引或接近全表扫描,就可能访问并锁住比目标订单更大的范围。复合索引应围绕真实过滤条件和选择性设计,同时评估写放大、索引维护成本与其他查询路径。
EXPLAIN 说明优化器计划访问哪些记录,但不会复现某一时刻实际持有的全部锁。高负载现场还可以查询 Performance Schema 的当前锁等待,把请求锁和阻塞锁对应起来:
-- 查看当前正在等待的锁以及对应的阻塞锁。 SELECT w.REQUESTING_ENGINE_TRANSACTION_ID AS waiting_trx, r.OBJECT_SCHEMA AS waiting_schema, r.OBJECT_NAME AS waiting_table, r.INDEX_NAME AS waiting_index, r.LOCK_TYPE AS waiting_lock_type, w.BLOCKING_ENGINE_TRANSACTION_ID AS blocking_trx, b.INDEX_NAME AS blocking_index, b.LOCK_MODE AS blocking_lock_mode FROM performance_schema.data_lock_waits AS w JOIN performance_schema.data_locks AS r ON r.ENGINE_LOCK_ID = w.REQUESTING_ENGINE_LOCK_ID JOIN performance_schema.data_locks AS b ON b.ENGINE_LOCK_ID = w.BLOCKING_ENGINE_LOCK_ID;
data_lock_waits 是当前等待关系的实时视图,不是历史死锁档案;查询时等待已经结束,就不会再看到对应记录。因此历史复盘应以错误日志和应用上下文为主,实时视图用于补充现场证据。
检查两个业务入口的访问顺序
把两个事务都展开到完整业务边界,按实际执行顺序列出会锁定的表和记录。最典型的问题不是某条 SQL 特别慢,而是两个入口对同一组资源采用了相反顺序。
-- 业务入口 A:先锁账户,再更新订单。 START TRANSACTION; SELECT id FROM accounts WHERE id = 1001 FOR UPDATE; UPDATE orders SET status = 'paid' WHERE tenant_id = 42 AND order_no = 'A1001'; COMMIT; -- 业务入口 B:先锁订单,再更新账户,顺序与入口 A 相反。 START TRANSACTION; SELECT id FROM orders WHERE tenant_id = 42 AND order_no = 'A1001' FOR UPDATE; UPDATE accounts SET balance = balance - 100 WHERE id = 1001; COMMIT;
如果两个入口都可能同时处理同一账户和订单,上述顺序就具备形成等待环的条件。排查时还要把 ORM 隐式查询、触发器、外键检查和批量更新纳入调用链,因为它们也可能改变真实锁顺序。对于一次锁多条同类记录的事务,应先按稳定键排序,例如统一按主键升序锁定,避免集合内部顺序随机。
把修复落到访问顺序与索引范围
第一项修复是让所有业务入口遵守同一顺序,例如统一“先账户、后订单”,并让批量记录按固定主键顺序访问。第二项是为精确谓词提供合适索引,让 SELECT ... FOR UPDATE 或 UPDATE 少扫描、少锁记录。第三项是缩短事务:网络调用、复杂计算和用户交互应尽量放到事务外,提交后及时释放锁。

即使顺序和索引已经优化,生产系统仍要把死锁视为可恢复的并发事件。InnoDB 检测到环后会回滚一个事务,应用应捕获死锁错误,从事务开始处重新读取数据并重放整个事务。重试次数要有限,使用短退避和抖动,并保证业务操作具备幂等约束;只重放失败 SQL 可能丢失该事务前面已回滚的修改。
NOWAIT 或 SKIP LOCKED 只适合明确接受“立即失败”或“跳过被锁记录”的特定队列语义,不能替代统一访问顺序。降低隔离级别也不是首选修复:它可能改变部分读锁或间隙锁行为,但写写冲突仍然可能死锁。
用同一组指标复查
上线后按改动前相同的时段、流量口径和业务入口复查,不要只看“当天有没有 1213”。先比较死锁次数和涉及的事务组合,再比较事务 P95、重试成功率、执行计划、扫描行数和错误日志量。若死锁减少但事务尾延迟明显上升,需要继续检查统一顺序是否把竞争集中到新的热点记录。
| 验收项 | 改动前记录 | 改动后记录 | 通过条件 |
|---|---|---|---|
| 同口径死锁次数 | ____ | ____ | 明显下降且事务组合符合预期 |
| 事务 P95 / P99 | ____ | ____ | 无不可接受的尾延迟回退 |
| 关键 SQL 所用索引 | ____ | ____ | 命中目标索引且扫描范围收敛 |
| 事务级重试成功率 | ____ | ____ | 有限次数内恢复,错误可观测 |
| 业务一致性抽查 | ____ | ____ | 账户、订单等关联状态一致 |
最后把“资源排序规则、事务边界、重试上限、索引假设和监控项”写进代码注释或工程规范。这样新增业务入口时可以复用同一锁顺序,而不是等下一次死锁后再从日志猜测。
常见问题
把隔离级别改成 READ COMMITTED 就不会死锁吗?
不会。它可能减少某些读场景中的间隙锁影响,但两个事务以相反顺序更新同一组记录时仍可能形成写写死锁。应先统一资源访问顺序并检查索引范围。
锁等待超时和死锁是一回事吗?
不是。死锁是等待关系形成环,InnoDB 检测后会主动选择受害事务回滚;锁等待超时是某个请求等待超过配置时间,未必存在环。两者都要保留事务上下文,但分析证据和修复重点不同。
什么时候适合开启 innodb_print_all_deadlocks?
适合在死锁间歇出现、最近一次记录容易被覆盖时短期开启。应先确认错误日志容量、权限和敏感信息处理方式,采样完成后关闭,避免长期增加日志量。
加了索引就能彻底消除死锁吗?
不能。合适索引可以减少扫描和锁定范围,但如果不同事务仍按相反顺序访问相同记录,等待环依然可能出现。索引、统一顺序、短事务和事务级重试需要一起设计。
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
501 收藏
-
394 收藏
-
376 收藏
-
243 收藏
-
228 收藏
-
500 收藏
-
252 收藏
-
488 收藏
-
164 收藏
-
303 收藏
-
184 收藏
-
153 收藏
-
110 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习