最稳妥方案是用 INSERT ON DUPLICATE KEY UPDATE 替代 REPLACE INTO,因其单路径加锁、无锁升级与死锁环路;但需注意返回值、多唯一索引响应顺序及字段更新语义差异。

REPLACE INTO 不是原子操作,它天然携带“失败重试”状态,这是所有复杂加锁的根源
REPLACE INTO 实际执行分两阶段:先插失败,再删+插
MySQL Server 层看到唯一索引冲突(如 DB_DUPLICATE_KEY)后,并不会直接跳到更新逻辑,而是先回滚第一步插入的聚簇索引记录(row_undo_ins),再重新走删除旧行 + 插入新行路径。这个“先加锁→失败→回滚→再加锁”的过程导致锁无法被及时释放:
- 第一阶段插入时,已在唯一索引上加了
LOCK_X | LOCK_ORDINARY(即 next-key 锁),该锁不会因回滚而释放 - 第二阶段定位冲突行时,又对主键和唯一索引重复加
LOCK_X行锁 - 最终事务持有多个位置的锁:原记录上的 record lock、左侧间隙上的 gap lock、新插入位置的 insert intention lock
唯一索引冲突时,间隙锁(gap lock)会意外残留
在 READ COMMITTED 隔离级别下,MySQL 通常不加 gap lock,但 REPLACE INTO 是特例——只要涉及唯一索引等值冲突,就会加 lock_mode X locks gap before rec。这是因为:
- InnoDB 在检测唯一冲突时,为防止幻读,需锁定“可能插入相同值”的间隙
- 即使最终执行的是 delete + insert,那个最初为“试插”加的 gap lock 仍全程持有,直到事务提交
- 典型表现:表
t(b)有唯一索引b,当前存在b = 8;并发执行REPLACE INTO t(b) VALUES (8)时,第一个事务会在(5, 8]或(8, 12)上持锁,阻塞INSERT INTO t(b) VALUES (6)等操作
多唯一键或主键+唯一键共存时,锁范围指数级扩大
当表有多个唯一约束(如主键 + UNI_C1 + UNI_C2),REPLACE INTO 的冲突处理逻辑会触发多次定位与加锁:
- 若新数据同时违反两个唯一键,InnoDB 可能先按索引顺序处理第一个冲突(比如
UNI_C1),加锁并删除对应行;再检查第二个唯一键(UNI_C2),发现仍冲突,又加锁、再删——导致同一事务持有两套 gap + record 锁 - 更危险的是:如果冲突行被其他事务刚标记为 delete-mark,
REPLACE INTO还需额外加锁等待 purge 线程清理,进一步延长锁持有时间 - 死锁高发场景:
REPLACE INTO t(b) VALUES (8)并发执行时,事务 A 持有(5, 8]gap lock 并等待INSERT_INTENTION,事务 B 持有(8, 12)gap lock 并等待对方释放,形成循环等待
真正难处理的不是锁本身,而是这些锁在事务生命周期内“不可见、不可控、不可预测”——它们不出现在 EXPLAIN 中,不随语句结束释放,也不受隔离级别常规规则约束。用 INSERT ... ON DUPLICATE KEY UPDATE 替代,才能绕过整个两阶段加锁模型。


















