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

MySQL 死锁排查工作流:从 InnoDB 状态到事务顺序优化

来源:17golang原创

时间:2026-06-17 16:43:15 392浏览 收藏

MySQL 死锁不是“数据库坏了”,而是两个或多个事务互相等待对方持有的锁,最后 InnoDB 主动回滚其中一个事务。线上看到的现象通常是接口偶发失败、日志里出现 deadlock、用户刷新后又好了。

这类问题最怕只盯着报错重试,而不去看事务访问顺序。本文按完整工作流梳理:从应用报错到 InnoDB 状态,从锁等待环到事务顺序优化,最后加上有限重试和上线检查点。

目录
  • 目标和边界
  • 全流程总览
  • 阶段一:抓到死锁现场
  • 阶段二:还原锁等待环
  • 阶段三:统一事务顺序和缩短锁持有时间
  • 阶段四:业务侧加有限重试保护
  • 我的推荐流程
  • 容易踩坑
  • 落地速查表

目标和边界

本文讨论的是 InnoDB 行锁场景下的典型死锁排查,不展开 MySQL 内核实现,也不把主题扩展成全量性能优化。读完后,你应该能完成三件事:

  1. 知道从哪里拿到最近一次死锁信息。
  2. 能根据事务、SQL、锁等待关系还原死锁链路。
  3. 能用统一加锁顺序、短事务和有限重试降低线上影响。

先说结论:死锁不是完全可以消灭的异常,尤其在高并发更新场景里,业务侧必须有重试保护。但重试不能替代治理,真正要修的是事务访问顺序、锁范围和锁持有时间。

全流程总览

一个实用的 MySQL 死锁排查流程可以拆成五步:应用报错、查看 InnoDB 状态、识别锁等待环、调整事务顺序、加重试保护。每一步都有明确产物,不能只停留在“日志里有 deadlock”。

MySQL 死锁排查从应用报错到重试保护的流程图

阶段 目标 关键动作 检查点
抓现场 确认是不是 InnoDB 死锁 记录错误码、接口、请求参数和事务入口 能关联到具体业务操作
看状态 读取最近一次死锁详情 使用 SHOW ENGINE INNODB STATUS 能看到两个事务和等待的锁
还原链路 找出循环等待关系 对比 SQL、索引、where 条件、加锁顺序 能画出谁等谁
修事务 减少再次死锁概率 统一访问顺序,减少事务内慢操作 高并发压测死锁次数下降
做保护 降低偶发死锁对用户的影响 有限重试、幂等校验、失败告警 重试次数可观测,失败可回溯

阶段一:抓到死锁现场

目标

先确认问题是不是死锁,而不是普通锁等待超时、连接池耗尽或接口超时。死锁通常会伴随 “Deadlock found when trying to get lock” 这类信息,被选中的事务会回滚。

关键动作

应用层日志至少记录四类信息:业务动作、请求 ID、SQL 所在方法、事务开始和结束位置。没有这些上下文,即使拿到了数据库死锁信息,也很难映射回代码。

request_id=pay-20260617-001
action=pay_order
db_error=Deadlock found when trying to get lock
tx_entry=PayService.payOrder
order_id=10086
sku_id=SKU-9

数据库侧先查看最近一次死锁详情:

SHOW ENGINE INNODB STATUS\G

如果线上死锁比较频繁,排查窗口内可以临时打开全部死锁记录,让信息进入 MySQL 错误日志。排查完成后要按团队规范评估是否关闭,避免日志过多影响观察。

SET GLOBAL innodb_print_all_deadlocks = ON;

常用工具/代码选择

小团队可以先用应用日志加 `SHOW ENGINE INNODB STATUS`;复杂系统建议把请求 ID、事务入口、SQL 模板和数据库错误码打到统一日志里,方便按时间线检索。

检查点

你应该能回答:哪一个接口触发了死锁、哪两个 SQL 参与了冲突、哪个事务被回滚、发生时间是否和业务峰值重合。

阶段二:还原锁等待环

目标

死锁排查的核心不是记住某条 SQL,而是还原循环等待:事务 A 拿着什么锁、还想要什么锁;事务 B 拿着什么锁、又在等什么锁。

关键动作

从 InnoDB 状态中摘出两个事务的 SQL、等待的索引、记录锁类型和回滚结果。然后回到业务代码,看这些 SQL 是不是在同一个事务里按不同顺序访问了相同资源。

事务 A:
1. 更新订单表 order_id=10086
2. 继续更新库存表 sku_id=SKU-9

事务 B:
1. 更新库存表 sku_id=SKU-9
2. 继续更新订单表 order_id=10086

结果:
A 等库存表,B 等订单表,形成循环等待。

还要检查索引是否命中。如果 where 条件没有走到合适索引,锁范围会扩大,原本只想改一行,结果锁住更多记录或间隙,死锁概率会被放大。

常用工具/代码选择

可以结合 `EXPLAIN` 看访问路径,用业务日志确认事务顺序,用压测脚本复现高并发场景。不要只在本地单线程点几次接口,那通常复现不出真实锁冲突。

检查点

排查到这一步,应该能画出一张等待关系图:事务 A 持有订单锁并等待库存锁,事务 B 持有库存锁并等待订单锁。画不出来,就说明证据还不够。

阶段三:统一事务顺序和缩短锁持有时间

修死锁时,最常见也最有效的思路是统一资源访问顺序。只要所有路径都按同一种顺序加锁,就能减少“你等我、我等你”的循环等待。

MySQL 反向加锁形成循环等待以及固定顺序短事务提交的对比图

目标

让所有涉及相同资源的事务按统一顺序访问,并尽量缩短锁被持有的时间。

关键动作

假设支付成功后要改订单和扣库存,团队可以约定所有事务都先锁订单,再锁库存。另一条补偿、取消、库存回写链路也要遵守同一顺序。

-- 推荐:所有入口都按相同顺序访问资源
START TRANSACTION;

SELECT id FROM orders
WHERE id = 10086
FOR UPDATE;

SELECT sku_id FROM stock
WHERE sku_id = 'SKU-9'
FOR UPDATE;

UPDATE orders
SET status = 'paid'
WHERE id = 10086;

UPDATE stock
SET quantity = quantity - 1
WHERE sku_id = 'SKU-9' AND quantity > 0;

COMMIT;

同时把事务里的非数据库动作移出去,例如远程接口调用、文件处理、复杂计算、消息发送。事务内只保留必须一起提交的数据库读写,锁持有时间越短,冲突窗口越小。

常用工具/代码选择

可以在服务层封装事务模板,把资源访问顺序写成团队约定;对核心表建立访问规范,例如订单相关事务先订单后明细,库存相关事务先库存主表后流水表。

检查点

检查所有入口:正向下单、取消订单、退款回滚、定时补偿、后台人工修复,是否都遵守同一加锁顺序。只改一个接口,另一个入口仍然反向访问,死锁还会回来。

阶段四:业务侧加有限重试保护

目标

即使事务顺序已经优化,高并发下仍可能出现偶发死锁。业务侧要把死锁当成可重试异常处理,但重试必须有限、幂等、可观测。

关键动作

只对明确的死锁错误做有限重试,例如最多 2 到 3 次,每次短暂退避。重试前确认业务动作具备幂等保护,例如订单支付不能重复扣款,库存扣减不能重复生成流水。

def run_with_deadlock_retry(action, max_retry=3):
    for i in range(max_retry):
        try:
            return action()
        except DeadlockError:
            if i == max_retry - 1:
                raise
            sleep_ms(50 * (i + 1))

如果重试成功,要记录一次可观测事件;如果最终失败,要把请求 ID、订单号、事务入口和重试次数打到告警里,方便后续复盘。

常用工具/代码选择

Java、Go、Python 都可以在数据访问层或应用服务层封装重试。关键不是语言,而是不要把所有数据库错误都重试,也不要无限重试。

检查点

上线后观察两条曲线:死锁次数是否下降,重试后成功率是否稳定。如果死锁次数不降反升,说明事务顺序或锁范围还没有治理到位。

我的推荐流程

真正处理线上死锁时,我建议按下面顺序来:

  1. 先记录应用错误码、请求 ID、业务参数和事务入口。
  2. 立刻查看 `SHOW ENGINE INNODB STATUS`,保存最近一次死锁文本。
  3. 把两个事务的 SQL 摘出来,标记它们持有什么锁、等待什么锁。
  4. 回到业务代码,查所有会访问这些表的入口,不只看报错接口。
  5. 统一资源访问顺序,补齐必要索引,移出事务内慢动作。
  6. 加有限重试和幂等保护,并把重试次数纳入监控。
  7. 用并发压测复现旧路径,再验证新路径死锁次数是否下降。

容易踩坑

坑点 表现 修法
只加重试 接口表面恢复,数据库死锁仍然很多 继续排查事务顺序和锁范围
只看一条 SQL 误以为某个 UPDATE 本身有问题 把两个事务的完整顺序都还原出来
索引缺失 where 条件锁到更多记录 补齐高选择性索引,并用访问计划确认
事务里做慢动作 锁持有时间变长,高峰期更容易冲突 把远程调用、消息发送、复杂计算移到事务外
补偿链路被忽略 主流程修好了,定时任务仍然反向加锁 统一梳理所有入口的资源访问顺序

落地速查表

检查项 最低要求 上线前确认
应用日志 记录请求 ID、业务参数、事务入口和数据库错误 能从日志定位到代码路径
死锁文本 保存 InnoDB 最近一次死锁详情 能识别两个事务和等待关系
索引检查 核心 where 条件命中合适索引 不会意外扩大锁范围
事务顺序 所有入口按同一资源顺序加锁 补偿、后台、定时任务也已检查
事务时长 事务内不放远程调用和慢逻辑 锁持有时间可被压测观察
重试保护 仅对死锁做有限重试,并保证幂等 重试成功率和最终失败都有监控

总结一下,MySQL 死锁排查要从“报错”走向“等待关系”,再从“等待关系”回到“事务设计”。把现场、锁环、顺序、索引、短事务和有限重试串起来,死锁就不再是只能碰运气的线上问题。

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