死锁四大源头:①多表更新顺序不一致;②INSERT ON DUPLICATE KEY UPDATE并发主键冲突触发间隙锁;③长事务混入HTTP调用等非DB操作致锁持有过久;④范围查询FOR UPDATE引发间隙锁扩散。

交叉更新多表时顺序不一致
这是最常见也最容易被忽视的死锁源头:事务A先改user再改order,事务B反着来——先改order再改user。只要两个事务并发执行、且都还没提交,就极可能各自持有一把锁、又卡在等待对方释放另一把锁上。
- 真实错误现象:
Deadlock found when trying to get lock; try restarting transaction(MySQL 错误码 1213) - 关键识别点:用
SHOW ENGINE INNODB STATUS\G查看LATEST DETECTED DEADLOCK段,会清楚列出两个事务各自的SQL和加锁行 - 实操建议:所有业务逻辑中涉及多表更新,必须硬性约定访问顺序,比如“用户→订单→物流”为全局铁律,哪怕某次只用到其中两张表,也要按顺序写
- 容易踩的坑:DAO 层封装了单表操作,但 Service 层组合调用时没统一协调顺序;或者不同微服务各自维护自己的事务逻辑,完全没对齐
INSERT ON DUPLICATE KEY UPDATE 并发插入主键冲突
当多个线程同时执行INSERT ... ON DUPLICATE KEY UPDATE,且目标记录尚不存在时,InnoDB 会对**相同索引值对应的间隙(gap)加 next-key lock**。如果插入顺序或范围重叠,极易触发间隙锁竞争,形成死锁。
- 典型场景:批量取消物流单,每个物流单对应多个订单,多线程并发处理时高频出现
-
为什么不是“先查后插”更安全?因为查+插是两步,中间有窗口期;而
ON DUPLICATE KEY UPDATE是原子的,但锁范围更大 - 实操建议:对高频冲突字段建唯一索引;若并发量极大,可考虑分片插入(如按物流单 ID 取模分批),或引入 Redis 预占位降低数据库争抢
- 容易踩的坑:以为“只是插入”,忽略了唯一索引带来的间隙锁行为;或误用非唯一索引导致全表扫描+表级锁升级
长事务里混入非DB操作
一个事务里做了数据库更新,接着调用 HTTP 接口、写本地日志、做复杂计算……这些操作不参与数据库锁管理,却让事务迟迟不提交,等于把行锁/间隙锁“霸占”住,其他事务只能排队等,死锁概率指数上升。
- 真实表现:死锁日志里看到某个事务 SQL 很简单(如
UPDATE user SET status=2 WHERE id=123),但等待时间超长 - 根本原因:InnoDB 的锁只在事务内有效,而事务生命周期被外部耗时操作拉长
- 实操建议:事务块严格限定为“纯DB操作”;HTTP 调用、日志、格式转换等一律移到
COMMIT之后;必要时拆成“预占+确认”两阶段 - 容易踩的坑:Spring
@Transactional注解套在含远程调用的方法上,没意识到整个方法体都被纳入事务边界
范围查询引发间隙锁扩散
执行类似SELECT * FROM order WHERE create_time > '2025-12-01' FOR UPDATE这种语句时,InnoDB 不仅锁定命中行,还会锁定满足条件的所有“间隙”。多个事务对同一范围做写操作,很容易因间隙重叠而互相阻塞。
- 为什么唯一索引能缓解?因为
WHERE id = 123这种等值查询(且id是主键或唯一索引)只会加 record lock,不加 gap lock - 实操建议:尽量用主键/唯一索引做精确更新;若必须范围操作,考虑降低隔离级别(如从
REPEATABLE READ调至READ COMMITTED,InnoDB 在该级别下不使用间隙锁) - 容易踩的坑:开发测试时数据量小,范围查询看似快且无锁;上线后数据膨胀,间隙锁覆盖范围剧增,死锁频发却难以复现
死锁不是“会不会发生”的问题,而是“什么时候爆发”的问题。真正难处理的,往往不是日志里明明白白写出的那两条 SQL,而是那些藏在事务边界外、由网络延迟、日志打点、缓存更新悄悄延长的锁持有时间。










