REPLACE INTO 在并发下极易触发死锁,因其内部执行 DELETE + INSERT 两阶段加锁且第一阶段锁不释放;而普通 INSERT 冲突时直接报错、锁立即释放,INSERT ... ON DUPLICATE KEY UPDATE 仅对冲突行加一次 X 锁并原地 UPDATE,死锁风险更低。

REPLACE INTO 在并发下极易触发死锁,而普通 INSERT 不会 —— 根本区别在于它内部执行的是 DELETE + INSERT 两阶段加锁,且第一阶段的锁不释放。
REPLACE INTO 的加锁流程导致死锁风险升高
普通 INSERT 只尝试插入一行,冲突时直接报错退出,锁在事务回滚或提交后立即释放;而 REPLACE INTO 遇到唯一键冲突时,会先对冲突行加 X 锁(用于后续 DELETE),再尝试插入新行 —— 这个过程中,DELETE 阶段持有的锁不会释放,直到整个语句完成。两个并发 REPLACE INTO 同时命中同一唯一值(比如都写 b = 8),就可能互相等待对方释放 next-key lock 和 insert intention lock,形成循环等待。
- 单次
REPLACE INTO实际执行:先 INSERT → 失败 → 加锁定位冲突行 → DELETE → INSERT 新行 - DELETE 阶段获取的 X 锁(如
8, 100 next-key X lock)在整个语句结束前一直持有 - 第二个并发事务在同样位置尝试 INSERT 时,会申请
insert intention lock,但被第一个事务的 X 锁阻塞 - 若此时第一个事务又试图插入另一行(或被调度顺序颠倒),就满足死锁四要素
INSERT ... ON DUPLICATE KEY UPDATE 更安全,但不是完全无锁
INSERT ... ON DUPLICATE KEY UPDATE 虽然也处理冲突,但它的锁行为更收敛:它只对冲突行加一次 X 锁,然后直接做原地 UPDATE,不触发 DELETE + INSERT 的两阶段锁膨胀。这意味着锁持有时间更短、范围更小,死锁概率显著低于 REPLACE INTO。
- UPDATE 操作复用原有行记录,
AUTO_INCREMENT值不跳变(除非显式更新主键) - 字段未出现在
UPDATE子句中时,旧值保留;而REPLACE INTO缺失字段会被设为默认值 - 即使并发写同一唯一键,只要 UPDATE 不涉及其他行或索引,通常只产生行级锁竞争,不构成死锁链
表结构决定 REPLACE INTO 是否真正“替换”
没有主键或唯一索引的表上,REPLACE INTO 等价于 INSERT,不会触发 DELETE 逻辑,自然也不会有额外死锁风险 —— 但这时它已失去“替换”意义,还可能因误用导致重复数据。
- 仅含主键:冲突时
DELETE + INSERT,AUTO_INCREMENT不递增 - 仅含唯一键:冲突时
DELETE + INSERT,AUTO_INCREMENT会 +1(新行获得新 ID) - 同时含主键和唯一键:冲突发生在唯一键时,
AUTO_INCREMENT+1;冲突发生在主键时,ID 不变 - 无任何唯一约束:
REPLACE INTO就是普通INSERT,binlog记为INSERT_EVENT
线上环境应优先用 INSERT ... ON DUPLICATE KEY UPDATE 替代 REPLACE INTO
除非你明确需要“清空旧行所有字段再重插”(比如字段多、默认值可控、且能保证每次写全字段),否则 REPLACE INTO 的锁行为和语义副作用(字段覆写、自增值跳变、binlog 类型混杂)在高并发场景下代价过高。
- 死锁不是偶发异常,而是可复现的并发路径 —— 用压测脚本能稳定触发
- MySQL 8.0+ 虽修复了部分死锁日志误导(如错误显示
HOLDS THE LOCK),但底层加锁逻辑未变 - 如果必须用
REPLACE INTO,务必确保写入字段完整,避免默认值污染;并控制并发度,或加应用层分布式锁
真正容易被忽略的点是:死锁往往不是因为“写得多”,而是因为“写得不够全”——字段缺失导致隐式默认值写入,进而引发意料之外的索引变更和锁范围扩大。


















