MySQL 死锁“破案”笔记:一次由索引引发的性能事故全记录

一、事故背景

之前在项目做性能优化时,为了“减少一次 RPC”,我们把一段 HTTP 调用改成了本地事务内直接 JDBC 更新。上线当天,监控突然飙红:

ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction

业务接口大面积 502,慢查询飙升到 3w+。
定位 10 分钟后确认:死锁!而且是“索引”导致的死锁。
下面把完整破案过程拆成 4 步,分享给你,照着抄作业就行。

二、MySQL 死锁的本质:90% 都是“索引”惹祸

InnoDB 的锁是加在索引上的,而不是记录本身。

  • 命中主键/唯一键:加行锁,粒度小,冲突概率低。

  • 命中普通二级索引:先锁二级索引,再回表锁聚簇索引,两把锁
    如果事务顺序相反,就会形成互相等待 → 死锁。

结论:死锁不一定是你代码“同时改同一行”,而是“通过不同索引改同一行”。

三、现场保留:3 条命令快速固定证据

  1. 看正在执行的会话

SHOW PROCESSLIST;

发现大量 Updating 状态,Time 飙到 30s+。

  1. 看当前锁等待

SELECT * FROM information_schema.innodb_locks\G

输出里出现两条记录,同一个 table_idlock_mode=Xlock_type=RECORD但 lock_index 不同:一条是 PRIMARY,一条是 idx_doc_id

  1. 看 InnoDB 最近一次死锁日志

SHOW ENGINE INNODB STATUS\G

LATEST DETECTED DEADLOCK 段里,清楚地打印了:

*** (1) WAITING FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 247 page no 8 n bits 96 index idx_doc_id of table `vrm`.`vrm_doc_art_ext`
*** (2) HOLDING THE LOCK:
RECORD LOCKS space id 247 page no 4 n bits 96 index PRIMARY of table `vrm`.`vrm_doc_art_ext`

交叉等待,实锤死锁。


四、代码层复盘:一次“本地事务 + HTTP”混用引发的血案

1. 老逻辑(无死锁)

@Transactional
public void updateExt(ArticleDTO dto){
    // 1. 本地更新 vrm_doc_art_ext
    jdbcTemplate.update("UPDATE vrm_doc_art_ext SET status=? WHERE doc_id=?", dto.getStatus(), dto.getDocId());
    // 2. HTTP 调用下游服务
    httpClient.post("http://inner/api/refresh", dto);
}

事务只在本地 JDBC 层面,锁顺序固定,无冲突。

2. 新逻辑(引入死锁)

为了“减少 RPC”,把 HTTP 调用改成了本地事务内再调一次本地方法

@Transactional
public void updateExt(ArticleDTO dto){
    // 1. 本地更新 vrm_doc_art_ext
    jdbcTemplate.update("UPDATE vrm_doc_art_ext SET status=? WHERE doc_id=?", dto.getStatus(), dto.getDocId());
    // 2. 本地调用,复用同一条事务
    refreshExt(dto);   // 内部又 UPDATE 同一表,但条件走了主键
}

两条 UPDATE 在同一个事务里,分别走了二级索引 idx_doc_id 和主键 PRIMARY,形成交叉持锁 → 死锁。

五、三种解决方案对比

方案改动量风险是否推荐
① 直接去掉 @Transactional1 行数据不一致❌ 业务无法接受
② 全部走 HTTP(回到老逻辑)回滚代码RT 升高✅ 最稳,可接受
③ 全部走本地 JDBC,统一索引重构 DAO需要回归✅ 性能最好,推荐

我们最终选了方案 3:

  • 把“更新状态”和“刷新扩展字段”合并成一条 SQL,统一用主键作为 WHERE 条件

  • 事务内只保留一次 UPDATE,锁顺序固定,死锁消失;

  • 压测 QPS 提升 18%,RT 降低 30%。

六、可复用的“防死锁” checklist

  1. 事务里任何更新必须走相同索引(最好都是主键)。

  2. 禁止在事务里交叉使用“本地 JDBC + HTTP/RPC 再回来改同一表”。

  3. 大事务拆小事务 + 异步消息,减少锁粒度。

  4. 上线前跑 mysqlslap 并发压测,观察 SHOW ENGINE INNODB STATUS 有无死锁。

  5. 打开 innodb_print_all_deadlocks = 1,让死锁日志直接落到 error log,方便后续审计。

七、一句话总结

死锁是索引顺序的冲突,不是 SQL 多少的冲突;
把事务内的索引路径“归一化”,80% 的死锁都会消失。


如果这篇文章帮你少加了一次班,欢迎点个 + 收藏 + 在看
评论区聊聊你踩过的死锁坑,一起“避坑”!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

项目治理之道

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值