空表并发INSERT必死锁是RR级别下Next-Key Lock导致所有事务在(-∞,+∞)间隙争抢Insert Intention Lock而形成循环等待;解决核心是绕过间隙锁,如用INSERT ON DUPLICATE KEY、降级到RC隔离级别或预占位。

空表或低数据量时并发 INSERT 必然死锁,这不是配置或重试能解决的,而是 RR 隔离级别下间隙锁(Gap Lock)的固有行为。
为什么空表 INSERT 会死锁?
REPEATABLE READ 下,InnoDB 对 INSERT 使用 Next-Key Lock(记录锁 + 间隙锁)。空表时,主键范围是 (-∞, +∞),所有事务都试图在同一个间隙加 INSERT INTENTION LOCK——它本身兼容,但因锁排队机制,在无锚定行的情况下形成循环等待。InnoDB 检测后回滚其中一个事务,报错:Deadlock found when trying to get lock; try restarting transaction。
典型现象:两条并发插入,一条成功(id=1),另一条死锁后重试成功(id=3),id=2 跳号。
- 这不是“运气差”,而是确定性行为:只要表为空、并发 INSERT、RR 级别,就必现
-
innodb_lock_wait_timeout调大没用——问题不在等待超时,而在锁序冲突本身 - 应用层重试只是掩盖症状,不减少锁竞争窗口
用 INSERT ... ON DUPLICATE KEY UPDATE 替代 SELECT + INSERT
很多业务写法是:SELECT ... FOR UPDATE 查是否存在 → 应用判断 → INSERT。这种模式极易死锁,尤其当 WHERE 字段没走唯一索引时,会升级为表锁或扩大间隙锁范围。
INSERT ... ON DUPLICATE KEY UPDATE 是原子操作,只锁命中行及其间隙,不触发额外的 SELECT 锁等待链:
- 必须确保字段有
UNIQUE KEY或PRIMARY KEY(如order_no建唯一索引) - 示例:
INSERT INTO orders (order_no, user_id) VALUES ('ORDER123', 1001) ON DUPLICATE KEY UPDATE user_id = VALUES(user_id) - 注意:它仍会申请插入意向锁,但避免了“查→判→插”三步锁分离带来的环路风险
降级隔离级别到 READ COMMITTED
在确认业务可接受“不可重复读”的前提下,把事务隔离级别设为 READ COMMITTED,InnoDB 会禁用间隙锁和 Next-Key Lock,只保留记录锁:
- 会话级执行:
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED - 或全局配置:
transaction_isolation = 'READ-COMMITTED'(写入my.cnf) - 此时
INSERT仅锁定实际插入的行,空表并发不再死锁 - 代价是幻读可能重现:事务 A 查询无某订单,事务 B 插入并提交后,A 再查就看到——若该场景不影响核心逻辑(如订单创建后不二次校验),可接受
索引结构与锁序不一致才是隐藏杀手
死锁多数不是因为插同一行,而是多个二级索引导致加锁顺序不一致。例如表有 INDEX idx_status_uid (status, user_id) 和 INDEX idx_uid_created (user_id, created_at),一个事务按 status 扫描,另一个按 user_id 扫描,二者在不同索引树上加锁路径天然冲突。
- 把高频共用字段(如
user_id)统一提到所有复合索引最左,例如改为idx_uid_status和idx_uid_created - 删除低频、高重复值的单列索引(如只有 3–5 个值的
status索引),它产生的 gap 锁范围大、竞争强 -
INSERT ... SELECT比批量VALUES更危险:前者被当作逐行处理,无法合并加锁,且ORDER BY或LIMIT可能改变扫描顺序,放大锁序不一致概率
真正难处理的从来不是单点插入,而是多索引、多表、长事务交织下的锁序不可控——哪怕你用了 ON DUPLICATE KEY,只要索引设计没对齐,死锁依然会发生。











