mysql并发insert死锁是rr隔离级别下间隙锁的必然行为:空表时所有事务在(-∞,+∞)间隙争抢insert intention lock,形成循环等待;唯一键冲突时s锁升级x锁亦引发死锁;根本解决需绕过间隙锁,如用insert on duplicate key、降级rc或预占位。

MySQL并发INSERT出现死锁,不是配置没调好、也不是SQL写错了,而是InnoDB在REPEATABLE READ隔离级别下对间隙(Gap)加锁的必然行为——尤其当表空或数据稀疏时,多个事务争抢同一段“虚拟间隙”,形成循环等待链。
空表或低数据量时并发INSERT必死锁
空表的主键/唯一索引范围是(-∞, +∞),所有并发INSERT都试图在同一个间隙申请Insert Intention Lock。这个锁本身兼容,但InnoDB内部会按顺序排队;没有实际记录锚定,事务无法判断“该插在哪”,最终触发死锁检测并回滚其中一个事务,报错:Deadlock found when trying to get lock; try restarting transaction。
- 典型现象:两条并发插入,一条成功(
id=1),另一条死锁后重试成功(id=3),id=2跳号 - 这不是偶发问题,只要隔离级别是RR、表无数据、并发INSERT相同结构(哪怕主键自增),就大概率复现
-
SHOW ENGINE INNODB STATUS里能看到lock_mode X locks gap before rec和insert intention waiting共存
唯一键冲突引发的Next-Key Lock升级死锁
当多个事务同时插入相同唯一值(如order_sn = 'SN_10086'),第一个事务成功插入并持有了该记录的隐式X锁;后续事务因Duplicate entry失败,但为检查冲突,必须去申请该唯一值所在位置的Next-Key S锁(即记录锁+间隙锁)。若此时第一个事务突然回滚,释放X锁,后两个事务就会同时尝试升级为X锁——但S锁之间不互斥,X锁却互斥,于是形成S→X升级竞争死锁。
- 要求:唯一索引存在(
UNIQUE KEY或PRIMARY KEY),且并发插入重复值 - 错误日志中常见
lock_mode S locks next key与lock_mode X locks rec but not gap交叉出现 - 这种死锁和空表死锁机制不同,但根因都是Next-Key Lock在RR下的锁粒度太宽
外键约束或联合唯一索引加剧间隙锁范围
如果表有外键,或唯一索引是联合索引(如(user_id, relation_id, relation_type)),而INSERT只提供部分字段(比如只填user_id),InnoDB无法精确定位间隙边界,会扩大锁住整个索引区间,进一步提高并发冲突概率。
- 例如
DELETE FROM user_relation WHERE user_id IN (...)+ 紧接着INSERT INTO user_relation,可能在联合索引上锁住大片间隙 - 即使没显式
SELECT FOR UPDATE,外键校验本身也会触发隐式加锁 - UUID主键+业务字段唯一索引的组合,因无顺序性,页分裂频繁,也容易诱发间隙锁争抢
真正难处理的不是“怎么避免死锁”,而是“为什么加了唯一索引、用了自增主键、开了RC级别,还是在某些边缘路径下死锁”——因为锁行为取决于执行路径上的索引命中情况、事务内操作顺序、甚至InnoDB页分裂瞬间的状态。线上排查必须结合SHOW ENGINE INNODB STATUS里的TRANSACTIONS和LOCK WAIT段,逐行比对锁模式与索引类型,不能只看SQL表面。











