一、事故背景
之前在项目做性能优化时,为了“减少一次 RPC”,我们把一段 HTTP 调用改成了本地事务内直接 JDBC 更新。上线当天,监控突然飙红:
ERROR 1213 (40001): Deadlock found when trying to get lock; try restarting transaction
业务接口大面积 502,慢查询飙升到 3w+。
定位 10 分钟后确认:死锁!而且是“索引”导致的死锁。
下面把完整破案过程拆成 4 步,分享给你,照着抄作业就行。
二、MySQL 死锁的本质:90% 都是“索引”惹祸
InnoDB 的锁是加在索引上的,而不是记录本身。
-
命中主键/唯一键:加行锁,粒度小,冲突概率低。
-
命中普通二级索引:先锁二级索引,再回表锁聚簇索引,两把锁;
如果事务顺序相反,就会形成互相等待 → 死锁。
结论:死锁不一定是你代码“同时改同一行”,而是“通过不同索引改同一行”。
三、现场保留:3 条命令快速固定证据
-
看正在执行的会话
SHOW PROCESSLIST;
发现大量 Updating 状态,Time 飙到 30s+。

-
看当前锁等待
SELECT * FROM information_schema.innodb_locks\G

输出里出现两条记录,同一个 table_id,lock_mode=X,lock_type=RECORD,但 lock_index 不同:一条是 PRIMARY,一条是 idx_doc_id。
-
看 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,形成交叉持锁 → 死锁。
五、三种解决方案对比
| 方案 | 改动量 | 风险 | 是否推荐 |
|---|---|---|---|
① 直接去掉 @Transactional | 1 行 | 数据不一致 | ❌ 业务无法接受 |
| ② 全部走 HTTP(回到老逻辑) | 回滚代码 | RT 升高 | ✅ 最稳,可接受 |
| ③ 全部走本地 JDBC,统一索引 | 重构 DAO | 需要回归 | ✅ 性能最好,推荐 |
我们最终选了方案 3:
-
把“更新状态”和“刷新扩展字段”合并成一条 SQL,统一用主键作为 WHERE 条件;
-
事务内只保留一次 UPDATE,锁顺序固定,死锁消失;
-
压测 QPS 提升 18%,RT 降低 30%。
六、可复用的“防死锁” checklist
-
事务里任何更新必须走相同索引(最好都是主键)。
-
禁止在事务里交叉使用“本地 JDBC + HTTP/RPC 再回来改同一表”。
-
大事务拆小事务 + 异步消息,减少锁粒度。
-
上线前跑
mysqlslap并发压测,观察SHOW ENGINE INNODB STATUS有无死锁。 -
打开
innodb_print_all_deadlocks = 1,让死锁日志直接落到 error log,方便后续审计。
七、一句话总结
死锁是索引顺序的冲突,不是 SQL 多少的冲突;
把事务内的索引路径“归一化”,80% 的死锁都会消失。
如果这篇文章帮你少加了一次班,欢迎点个 赞 + 收藏 + 在看。
评论区聊聊你踩过的死锁坑,一起“避坑”!


被折叠的 条评论
为什么被折叠?



