批量插入死锁典型场景是InnoDB在RR隔离级别下,因Insert Intention Lock与Gap Lock互斥引发循环等待:当多事务交叉插入同一索引间隙或唯一键冲突时,各自持有间隙一侧锁并争抢对方间隙,触发死锁。

批量插入死锁的典型触发场景
批量插入(INSERT ... SELECT、INSERT VALUES (...), (...) 或 ORM 批量 save)在高并发下极易引发死锁,尤其当涉及唯一索引冲突、间隙锁(Gap Lock)或多个事务交叉插入相邻主键区间时。最常见现象是应用日志反复出现 ERROR 1213 (40001): Deadlock found when trying to get lock,而单条插入完全正常。
根本原因不是“插入本身会死锁”,而是 InnoDB 在 RR 隔离级别下对插入路径的加锁行为:
- 即使插入不重复,InnoDB 仍需在目标位置加
Insert Intention Lock(插入意向锁),该锁与间隙锁(Gap Lock)互斥 - 若两个事务试图插入同一索引间隙(如
id BETWEEN 100 AND 200),且各自已持有该间隙一侧的锁(例如事务 A 持有(50,100]的 next-key lock,事务 B 持有[100,150)的 gap lock),再互相请求对方间隙 → 死锁 - 唯一索引冲突时(如重复
email),InnoDB 会对冲突值加S lock,此时若另一事务正对该行做UPDATE ... FOR UPDATE,也会形成 X/S 冲突
SHOW ENGINE INNODB STATUS 是唯一可信现场证据
别猜 SQL 顺序,直接看死锁快照。执行 SHOW ENGINE INNODB STATUS\G,聚焦 LATEST DETECTED DEADLOCK 区域:
- 找
*** (1) TRANSACTION和*** (2) TRANSACTION下的query id和完整 SQL —— 确认是不是你代码里的批量插入语句 - 看
WAITING FOR THIS LOCK TO BE GRANTED行:若出现gap lock或insert intention lock,基本锁定是间隙竞争问题 - 查
HOLDS THE LOCK(S)中的space id和page no,结合information_schema.INNODB_SYS_INDEXES反查是哪张表哪个索引
注意:默认只保留最近一次死锁。线上务必提前开启 innodb_print_all_deadlocks = 1,避免问题复现时无迹可寻。
批量插入死锁的四类实操解法
没有银弹,需按场景选型:
-
统一插入顺序 + 主键预排序:若批量插入含主键(如
INSERT INTO t(id, name) VALUES (3,'a'),(1,'b'),(2,'c')),InnoDB 内部按主键物理顺序加锁。强制客户端按id ASC排序后再拼 SQL,可消除因乱序导致的锁交叉 -
拆成单条 + 重试机制:对敏感业务(如支付订单),放弃批量语法,用循环执行单条
INSERT ... ON DUPLICATE KEY UPDATE,捕获1213错误后指数退避重试。虽吞吐略降,但确定性高 -
临时降级隔离级别:在事务开始前执行
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED。RC 级别下间隙锁失效,极大降低插入死锁概率(但需确认业务能接受幻读) - 绕过唯一索引冲突检测:若批量插入前可预判重复(如先
SELECT id FROM t WHERE email IN (...)),则只插入非重复数据;或改用INSERT IGNORE避免 S 锁争抢
ORM 层最容易被忽略的陷阱
MyBatis、GORM、Django ORM 等框架自动生成的批量 SQL,往往隐藏致命细节:
- GORM 的
CreateInBatches默认按传入切片顺序插入,若 slice 元素未按主键排序,等同于人为制造乱序插入 - MyBatis
<foreach>拼接 values 时,若 collection 是 HashMap 或无序集合,values 顺序不可控 - Django 的
bulk_create在 MySQL 后端下,若模型含auto_now_add字段,可能触发隐式时间戳计算,延长事务时间 → 放大锁持有窗口
真正有效的防御,不是加更多重试,而是把「插入顺序」和「事务边界」显式暴露在代码中——批量操作前排序、单事务内只做必要写入、冲突逻辑前置校验。死锁不会消失,但可以变得完全可预测。


















