rc下死锁并非因间隙锁消失,而是唯一键冲突仍会加next-key lock;并发执行insert ... on duplicate key update或replace into时,因冲突校验在冲突行或其后继加x型锁,易形成循环等待。

RC下死锁不是因为间隙锁消失,而是因为唯一键冲突仍会加锁
很多人误以为切换到 READ COMMITTED 就能彻底避开死锁,其实不然。RC 确实取消了普通 SELECT ... FOR UPDATE 或范围 INSERT 的间隙锁,但只要涉及唯一约束(主键或唯一索引)的冲突检测,InnoDB 依然会加 NEXT-KEY LOCK —— 这是 RC 下死锁的根源。
典型场景是并发执行 INSERT ... ON DUPLICATE KEY UPDATE 或 REPLACE INTO,当多个事务试图插入相同唯一值时:
- 每个事务先检查唯一键是否存在(触发唯一性校验)
- 若发现冲突,InnoDB 在冲突记录及其后继位置加 X 型
NEXT-KEY LOCK - 如果事务 A 锁 (30,40],事务 B 同时锁 (40,50],而它们又都尝试插入 40,就可能因锁范围重叠+等待顺序形成环路
REPLACE 和 INSERT ON DUPLICATE KEY UPDATE 的锁行为差异很关键
这两类语句在 RC 下都可能引发死锁,但加锁位置不同,容易被忽略:
-
INSERT ... ON DUPLICATE KEY UPDATE:冲突时,在**冲突行本身**加 X 型NEXT-KEY LOCK -
REPLACE INTO:冲突时,在**冲突行的下一条记录**加 X 型NEXT-KEY LOCK(例如插入(40, ...)冲突,则锁(40,50]而非(30,40]) - 这个细微差别会让两个事务实际锁定的索引区间错位,反而更容易交叉等待
死锁日志里常出现的 insert intention waiting 是什么
看到死锁日志中反复出现 insert intention waiting,说明问题不在“谁先抢到锁”,而在于“谁在等谁释放 GAP 锁”。即使在 RC 下,INSERT 前仍需获取插入意向锁,而该锁会被已持有的 GAP LOCK 阻塞 —— 这种阻塞链一旦成环,InnoDB 就判定为死锁。
常见诱因包括:
- 表上有唯一联合索引(如
(a,b)),并发写入时按不同顺序命中索引前缀 - 事务中先
SELECT ... FOR UPDATE读某范围,再INSERT相邻值(RC 下该SELECT不加 GAP 锁,但后续INSERT可能因唯一检查触发) - 批量导入用循环单条
REPLACE,且 key 分布密集
真正有效的缓解手段不是换隔离级别,而是控制写入模式
把 RR 改成 RC 只是降低概率,不能根除。更可靠的做法是收敛写入逻辑:
- 所有写唯一键的语句,确保 key 值按固定顺序生成(如对
REPLACE的 key 先ORDER BY) - 避免在事务内混合使用
SELECT ... FOR UPDATE和INSERT ... ON DUPLICATE KEY UPDATE - 对高频冲突场景(如分布式锁表),改用
SELECT ... FOR UPDATE+ 显式INSERT或UPDATE,而非依赖冲突语法 - 监控
innodb_row_lock_waits和死锁日志中的index uk_...名称,定位具体是哪个唯一索引在“拉偏架”
RC 下死锁往往藏得更深:没有显式的范围锁提示,只在唯一键碰撞瞬间发生,查日志时容易只盯着 SQL 表象,忽略索引结构和插入顺序这两个隐形推手。











