rc级别下insert on duplicate key update仍会死锁,根本原因是唯一二级索引冲突触发隐式x锁与插入意向锁的循环等待;innodb在rc下虽不加间隙锁,但为唯一键冲突检测必须对二级索引记录加record-level x锁,且加锁顺序不一致导致死锁。

RC级别下依然死锁,根本原因不是“间隙锁失效”没生效,而是唯一二级索引冲突触发了隐式X锁 + 插入意向锁(Insert Intention Lock)的等待链。
为什么RC级别下INSERT ON DUPLICATE KEY UPDATE还会死锁
很多人误以为RC隔离级别就完全不加间隙锁,但官方文档明确说明:gap locking is used only for foreign-key constraint checking and duplicate-key checking。也就是说,只要涉及唯一键(主键或UNIQUE索引)冲突判断,InnoDB就必须加锁防止并发插入覆盖同一键值——哪怕在RC下也逃不掉。
典型死锁场景是两个事务按不同顺序尝试插入/更新同一组唯一键:
- 事务A执行
INSERT ... ON DUPLICATE KEY UPDATE插入songid=16,先获取该二级索引记录上的X锁(因冲突检测需要排他访问) - 事务B同时执行同语句插入
songid=17,也拿到对应X锁 - 接着A尝试插入
songid=17,发现已被B加X锁,开始等待 - B尝试插入
songid=16,发现已被A加X锁,也等待 → 循环等待成立
注意:这个X锁不是加在聚簇索引(主键)上,而是直接加在二级唯一索引记录上(uk_column_key),因为冲突检测第一步就是查二级索引。
如何确认死锁是否源于二级索引加锁
关键看 SHOW ENGINE INNODB STATUS\G 输出中的 WAITING FOR THIS LOCK TO BE GRANTED 和 HOLDS THE LOCK(S) 部分:
- 若出现
index PRIMARY但WHERE条件里根本没用主键(比如只用了WHERE uk_column = ?),说明优化器回表时被迫锁了主键——这是索引设计缺陷 - 若明确看到
index uk_column_key(你的唯一二级索引名),且锁模式是X locks rec but not gap,基本可锁定问题根源 - 若出现
insert intention和gap before rec并存,说明事务已在间隙上申请插入意向锁,但被其他事务的X锁阻塞
不要只盯着“PRIMARY”,很多死锁实际起始于二级索引——InnoDB对唯一索引的冲突检查是原子的,必须先锁二级索引记录,再决定是否回表加主键锁。
避免二级索引死锁的实操方案
核心思路是打破“不同顺序加锁”这个前提,让所有事务以相同逻辑路径访问数据:
- 批量操作前,先用
SELECT ... FROM table WHERE uk_column IN (...) ORDER BY uk_column FOR UPDATE预加锁,强制按索引顺序获取锁(ORDER BY是关键) - 改用
REPLACE INTO?不行——它本质是DELETE + INSERT,会引入更多锁阶段,死锁风险更高 - 业务层做写入顺序收敛:例如所有客户端统一按
uk_column % N分桶,每个桶内串行处理,从源头消灭并发冲突 - 临时降级为
INSERT IGNORE+ 单独UPDATE,但要注意:这会丢失“冲突时原子更新”的语义,且两次网络往返增加延迟
最稳妥的长期解法是:确保二级唯一索引字段具备天然有序性(如时间戳+序列号组合),并让应用始终按该字段单调递增写入——这样InnoDB加锁顺序天然一致,死锁概率趋近于零。
RC下二级索引加锁最容易被忽略的点
很多人以为“RC不加gap lock”就能高枕无忧,却忽略了三个隐蔽事实:
-
INSERT ... ON DUPLICATE KEY UPDATE在RC下仍会对**唯一二级索引记录本身加X锁**(不是gap,是record-level X lock),只为做duplicate check - 即使你没显式写
FOR UPDATE,任何DML都是当前读,都会触发加锁;快照读(普通SELECT)才不加锁 - 如果唯一索引是联合索引(如
UNIQUE KEY (a,b)),而SQL只命中a,InnoDB会升级为范围锁(gap + record),此时RC也无法避免间隙影响
真正决定锁行为的,从来不是隔离级别开关,而是SQL怎么写、索引怎么建、数据怎么分布——别信“RC就安全”的经验主义。











