MySQL 死锁排查与根治:从日志取证到索引优化
前言
线上数据库每隔几分钟抛一次死锁告警,日志里两个事务交叉等待同一批表资源——这是很多后端同学都遇过的场景。常见的应急动作是调隔离级别、缩短事务,但很多时候告警依旧。这篇就把一套完整的死锁排查路径写清楚:从日志取证、锁定冲突对,到索引优化与加锁顺序的根治手段。
第一步:拿到死锁现场
死锁发生时,InnoDB 会自动回滚代价较小的事务。要看现场,第一反应应该是:
SHOW ENGINE INNODB STATUS\G
输出里的 LATEST DETECTED DEADLOCK 段是重点,包含:
- 事务 1 持有的锁(HOLDS THE LOCK(S))与正在等待的锁(WAITING FOR)
- 事务 2 的同样两组信息
- 每把锁关联的 SQL 语句和 index name
问题是这段信息只保留最近一次死锁,高并发场景下可能被刷掉。更可靠的做法是开启全量死锁日志:
SET GLOBAL innodb_print_all_deadlocks = ON;
之后每次死锁都会写入 error log,时机不再是问题。云上 RDS 同样支持该参数(动态生效),配合日志告警可以形成完整的证据链。
第二步:读懂锁记录
日志里每把锁的记录形如:
lock_mode IX
lock_rec X locks rec but not gap
index name of lock: idx_order_user
两类典型形态:
形态一:两把行锁都带索引名。这是二级索引行锁冲突——事务 A 按索引 1 锁了行 X 再请求行 Y,事务 B 按索引 2 锁了行 Y 再请求行 X,形成环路。这时把日志里的 SQL 逐条 EXPLAIN,重点看执行计划走了哪个索引、扫描范围多大。经常发现的情况是:两个事务用了不同的查询路径(一个走 idx_a,一个走 idx_b),但目标行集有交集,各自按不同顺序加锁。
形态二:出现大量 gap lock 或 table 级锁。通常是 RR 隔离级别下区间锁碰撞,或者 WHERE 条件无索引导致锁范围放大——等价于在扫描路径上加锁,冲突面指数级增长。
第三步:根治手段按优先级排
1. 补对索引(性价比最高)
死锁日志里看到锁落在 PRIMARY、但 SQL 的 WHERE 走不到任何二级索引时,基本可以断定是全表扫描路径加行锁。按业务实际查询条件补联合索引,让每个事务只锁自己真正要动的行,冲突概率骤降。
2. 统一加锁顺序
应用层约定所有事务按相同顺序访问表和行。比如批量更新列表时统一按主键升序处理,交叉等待直接消失。这一条改动成本几乎为零,效果却出奇地好。
3. 缩小事务颗粒度
- 批量操作拆批,单事务行数控制在几百行以内
SELECT ... FOR UPDATE的 WHERE 带精确索引条件,避免锁到不相关的行- 事务里不要夹外部调用(RPC、发消息),把耗时操作挪出事务边界
4. 评估隔离级别
业务不依赖可重复读时,改 READ COMMITTED 能消掉大部分 gap lock 引发的死锁。改之前务必确认业务侧没有幻读依赖(比如对账类逻辑)。
5. 应用层重试兜底
InnoDB 死锁会自动选择牺牲者回滚,应用层捕获 1213 错误码做指数退避重试。这治标不治本,但能保住可用性,配合前面几条根治手段一起上。
第四步:持续观测防复发
调整上线后,用观测手段验证效果:
- MySQL 8.0+ 查
performance_schema.data_lock_waits,直接看"谁在堵谁" sys.innodb_lock_waits视图输出格式更友好- 云数据库控制台的锁等待指标配告警,死锁率上升能第一时间感知
一个真实案例的排查轨迹
某订单服务死锁告警,日志显示事务 A 持有 idx_user 上的行锁等待 PRIMARY,事务 B 反过来。EXPLAIN 发现 B 的查询 WHERE 条件只命中 idx_status,而该索引区分度极低(状态只有 3 个枚举值),导致扫描范围覆盖大量行。处理方式分两步:给查询条件补 user_id + status 联合索引;批量操作统一按主键升序。上线后死锁告警归零。
多数"调了隔离级别、缩短了事务还是死锁"的案例,最后都指向索引缺失和加锁顺序不统一这两个根因。排查时优先从这两处下手,往往事半功障。
写在最后
死锁不可怕,可怕的是没有证据链的盲调。先 SHOW ENGINE INNODB STATUS / innodb_print_all_deadlocks 固定现场,再从锁记录反推 SQL 与索引,最后用索引优化 + 顺序统一收尾——这套流程走下来,绝大多数死锁都能根治。
- 点赞
- 收藏
- 关注作者
评论(0)