insert on duplicate key update 在冲突行上直接加 x 行锁(x,rec_not_gap),非先 s 后 x;唯一键不存在时加 x next-key 锁防幻读;多唯一键或隐式转换会扩大锁范围、增加死锁风险;replace into 锁开销更大且有副作用,应避免滥用。

INSERT ON DUPLICATE KEY UPDATE 在冲突行上加的是 X 行锁,不是 S 锁
网上有些资料误传它“先加 S 锁再升级为 X 锁”,这是错的。MySQL 实际在 INSERT 查找阶段就直接加 X 锁(或 X next-key 锁),不存在共享锁过渡。只要唯一索引能准确定位到某一行(比如 PRIMARY 或 uniq_i1 匹配成功),就会对那行加 X,REC_NOT_GAP —— 即排他记录锁,等同于 SELECT ... FOR UPDATE 的锁强度。
常见错误现象:Deadlock found when trying to get lock,往往是因为两个事务同时尝试插入相同唯一键值,各自抢先持有了该行的 X 锁,又互相等待对方释放——根本没机会走到 UPDATE 阶段,锁已在查找时定型。
唯一键不存在时加的是 X next-key 锁,覆盖间隙 + 虚拟记录
当你要插入的值在唯一索引中查不到(比如空表首次插入 user_id = 1001),InnoDB 会锁定「该值应插入的位置」,也就是一个 next-key 锁:包含间隙(gap)和右侧边界(supremum record)。例如在 (0, 1001) 间隙加锁,防止其他事务在此间插入新行,避免幻读。
这个锁范围受隔离级别影响(REPEATABLE-READ 下必加 next-key;READ-COMMITTED 下只加 record lock,但仅限于已存在行,对间隙不锁);也受索引结构影响:
- 联合唯一索引如
UNIQUE KEY (a, b),只用到a = ?时,锁范围由a的等值匹配决定,b列是否命中不影响锁定位 - 如果
WHERE条件触发隐式类型转换或函数包裹(如WHERE i1 + 0 = 12),可能导致唯一索引失效,InnoDB 无法精确定位,被迫扩大锁范围甚至退化为意向锁等待
多唯一键共存时,锁顺序不确定,死锁风险陡增
一张表如果有多个唯一键(比如 PRIMARY KEY(id) + UNIQUE KEY uk_user_id(user_id)),InnoDB 检查冲突的顺序取决于索引创建顺序(非字母序、非定义顺序),而这个顺序在主从复制中可能不一致——主库按 A-B 添加索引,从库按 B-A 添加,就会导致同一语句在主从上加锁路径不同,引发数据不一致或死锁。
典型死锁场景(空表下极易复现):
- T1 插入
user_id = 1001→ 加间隙锁(0, 1001) - T2 插入
user_id = 1003→ 加间隙锁(1001, 1003) - T3 插入
user_id = 1002→ 需要同时申请(1001, 1003),被 T2 阻塞 - T1 再次插入
user_id = 1002→ 同样卡在(1001, 1003),此时 T1 等 T2,T2 等 T3,T3 等 T1,环路形成
REPLACE INTO 和 INSERT IGNORE 的锁行为差异很关键
三者表面功能相似,但底层加锁逻辑完全不同:
-
INSERT ... ON DUPLICATE KEY UPDATE:查→锁→(冲突则 update);只锁冲突涉及的唯一键对应行/间隙;原子性强 -
INSERT IGNORE:查→锁→(冲突则跳过);锁行为与上者完全一致,只是不执行 update -
REPLACE INTO:查→锁(delete 阶段加 X record lock)→删→锁(insert 阶段加 X next-key lock)→插;多一次 delete 锁开销,且会触发外键级联、自增 ID 跳变、触发器执行
所以除非你明确需要「整行替换+副作用触发」,否则不要用 REPLACE INTO 替代 ON DUPLICATE KEY UPDATE。另外,ON DUPLICATE KEY UPDATE 不支持对不同唯一键冲突做差异化处理(比如 A 键冲突 update,B 键冲突 ignore),这点必须靠应用层兜底。
真正容易被忽略的是:锁是在 INSERT 的「查找阶段」就完成的,不是在 UPDATE 执行时才加。这意味着即使你写的 UPDATE 子句是空操作(比如 SET updated_at = updated_at),只要冲突发生,锁照样加上,且持续到事务结束。别指望“没真更新”就能躲过锁竞争。











