INSERT语句在RR隔离级别下会触发行锁和间隙锁,因插入意向锁、唯一键冲突时加S锁及gap锁重叠,易引发死锁;并发插入相同唯一键时,多个事务持S锁等待升级为X锁即构成循环等待。

INSERT 语句本身就会触发行锁和间隙锁
很多人误以为 INSERT 是“纯新增”,不涉及锁竞争。实际上,在 InnoDB 的可重复读(RR)隔离级别下,INSERT 不仅会对插入的记录加排他锁(X lock),还会根据 WHERE 条件或唯一索引冲突情况,隐式加间隙锁(gap lock)或共享锁(S lock)。尤其是当插入值落在已有索引范围的间隙中,或者触发唯一键冲突时,InnoDB 必须防止幻读或重复插入,就会主动上锁。
常见错误现象:
- 并发执行
INSERT INTO t1 VALUES (1)多次,其中一条成功,其余报Deadlock found when trying to get lock(不是主键冲突错误) - 死锁日志里显示两个事务都在等待同一行的
X锁,但那行实际并不存在 -
SHOW ENGINE INNODB STATUS\G中的LOCK WAIT显示持有S锁、等待X锁
唯一索引冲突时会加共享锁(S lock)
这是最反直觉但高频的死锁诱因。当多个事务同时尝试插入相同唯一键(如 order_no)时,第一个事务获得 X 锁并插入成功;后续事务因主键/唯一键冲突失败,但 MySQL 会**在冲突记录上加 S 锁**(见官方文档:“If a duplicate-key error occurs, a shared lock on the duplicate index record is set”)。此时若第一个事务回滚,它释放 X 锁,剩下两个持 S 锁的事务就会互相等待升级为 X 锁——形成死锁。
实操建议:
- 避免用
INSERT ... SELECT或应用层“先查后插”做幂等控制,改用INSERT IGNORE或ON DUPLICATE KEY UPDATE - 如果必须校验存在性,用
SELECT ... FOR UPDATE时确保 WHERE 条件走**唯一索引**(比如WHERE order_no = ?且order_no是 UNIQUE KEY),这样只锁单行,不锁间隙 - 对非唯一索引字段做
SELECT FOR UPDATE(如普通INDEX order_no),会触发 next-key 锁(记录锁 + 间隙锁),极易引发多事务间隙重叠导致死锁
多事务按不同顺序插入相邻间隙
当表中有索引值 10、20、30,两个事务分别尝试插入 15 和 18,且都使用了 SELECT ... FOR UPDATE 校验间隙时,InnoDB 可能对 (10,20) 这个间隙加锁。但由于加锁顺序不可控(例如事务 A 先锁住 (10,20),事务 B 先锁住 (15,18) 内部更小的子间隙),就可能形成循环等待。这种场景在订单号、流水号等递增但非严格连续的业务中很常见。
关键参数差异:
- 如果插入字段是主键或唯一索引,InnoDB 通常只加 record lock(不锁间隙)
- 如果插入字段是**非唯一索引**,或 WHERE 条件未命中索引,InnoDB 会加 next-key lock(record + gap),锁范围扩大数倍
-
innodb_lock_wait_timeout控制等待上限,但不解决根本冲突;innodb_deadlock_detect=ON(默认)能快速发现并回滚,但频繁发生说明逻辑有缺陷
为什么 ORDER BY id 排序能降低 INSERT 死锁概率?
批量插入多条记录时,例如 INSERT INTO t VALUES (3),(1),(2),InnoDB 内部会按主键顺序加锁(即先锁 1、再 2、再 3)。但如果应用层拼接 SQL 时 ID 乱序,又没显式排序,多个事务插入相同 ID 集合但顺序不同,就容易出现加锁顺序不一致——这正是死锁占比超 60% 的主因。
实操建议:
- 所有批量
INSERT的VALUES列表,务必在应用层按主键或唯一键升序排列 - 用
INSERT ... SELECT时,在子查询末尾加上ORDER BY id(即使目标表无主键,也要确保输入有序) - ORM 框架如 MyBatis 动态生成
IN条件时,禁止直接用#{list}原样传入,必须提前Collections.sort()或数据库侧ORDER BY
真正难处理的从来不是单条 INSERT 的锁行为,而是多个事务对同一索引间隙的感知偏差和加锁节奏错位。排查时盯紧 SHOW ENGINE INNODB STATUS\G 里的 TRANSACTIONS 和 LOCKS 区块,比猜逻辑更可靠。


















