根本原因是多个事务对相同资源加锁顺序不一致,叠加RR隔离级别下的行锁与间隙锁机制,形成循环等待;需统一加锁顺序、确保索引命中、避免无序批量更新,并通过SHOW ENGINE INNODB STATUS定位逆序根源。

MySQL 在高并发更新时频繁死锁,根本原因不是并发本身,而是多个事务对相同资源加锁的**顺序不一致**,叠加 InnoDB 的行锁 + 间隙锁机制,在 RR 隔离级别下极易形成循环等待。死锁不是性能瓶颈,而是逻辑冲突的明确信号。
为什么 UPDATE 语句容易触发死锁
死锁几乎都发生在写操作(尤其是 UPDATE / DELETE / SELECT ... FOR UPDATE)中,因为这些语句会主动申请排他锁(X 锁)。关键点在于:
- InnoDB 的行锁只在**走索引**时生效;如果
WHERE条件没命中索引,会升级为全表扫描 + 全表加 X 锁或大量间隙锁,大幅提高交叉锁定概率 - 即使走了索引,若条件是范围查询(如
id > 100),InnoDB 会在匹配区间内加 Next-Key Lock(记录锁 + 间隙锁),锁住“本不该动”的空隙 - 两个事务分别执行
UPDATE t SET x=1 WHERE id=5和UPDATE t SET x=2 WHERE id=3不会死锁;但若事务 A 先锁 3 再锁 5,事务 B 先锁 5 再锁 3,就满足循环等待条件
SHOW ENGINE INNODB STATUS 看到的死锁日志到底在说什么
每次发生死锁,MySQL 自动回滚一个事务,并把现场快照写进 SHOW ENGINE INNODB STATUS 的 LATEST DETECTED DEADLOCK 段。你需要盯住三处:
-
事务持有的锁:看
HELD LOCKS行,确认它锁了哪些索引、哪几行(如PRIMARY索引上space id 123 page no 1024 n bits 72) -
事务等待的锁:看
WAITING FOR THIS LOCK TO BE GRANTED,明确它卡在哪条记录、什么锁类型(X还是S,是否带GAP) - SQL 执行顺序:对比两个事务各自的 SQL 执行流,找出谁先锁了 A 后等 B,谁先锁了 B 后等 A —— 这就是逆序根源
注意:innodb_print_all_deadlocks = 1 必须提前配置,否则错误日志里只保留最后一次死锁,线上排查会丢关键线索。
为什么加了索引还是死锁
索引只是前提,不是万能解药。以下情况即使有索引也会死锁:
- 复合索引字段顺序与查询条件不匹配,导致索引失效(如索引是
(a,b),却查WHERE b = ?) - 事务中混合使用主键和二级索引更新同一行(例如事务 A 用主键
id更新,事务 B 用唯一索引order_no更新同一行),InnoDB 会为两个索引分别加锁,扩大冲突面 - 批量更新未排序:比如
UPDATE t SET status=1 WHERE id IN (5,3,8),InnoDB 内部按索引物理顺序加锁(可能是 3→5→8),而另一事务传入(8,3,5)就可能错序 - ORM 自动生成 SQL 时,
WHERE子句字段顺序不固定(如 MyBatis 动态 SQL 中<if test="x!=null">插入顺序不可控)
应用层重试不是兜底,而是暴露设计缺陷
捕获 Deadlock found when trying to get lock 并自动重试,只能缓解表象。真正要警惕的是:
- 重试后仍失败 → 说明不是偶发竞争,而是结构性冲突(比如两个服务都在抢同一组用户 ID)
- 重试集中在某几张表/某些 ID 区间 → 暴露热点数据问题,需考虑分片、缓存或队列削峰
- 重试间隔太短(如立即重试)→ 可能加剧锁竞争,反而推高死锁率
最常被忽略的一点:死锁日志里出现的 SQL,往往不是业务主干逻辑,而是审计日志、状态同步、库存扣减等“辅助流程”——它们更容易被忽视索引和事务边界。


















