最稳妥方案是用 insert ... on duplicate key update 替代 replace into,因其单路径加锁、无锁升级与死锁环路;但需注意返回值、多唯一索引响应顺序及字段更新语义差异。

直接换用 INSERT ... ON DUPLICATE KEY UPDATE 是最稳妥、开销最小、且能彻底绕过死锁根源的方案。它不触发 delete + insert 的两阶段加锁,也不会在插入间隙中申请插入意向锁(LOCK_INSERT_INTENTION),天然规避了 replace into 在并发下因锁升级和锁等待环路导致的死锁。
为什么 replace into 在并发下必现死锁风险
核心在于它的执行不是原子操作,而是“先插失败 → 再删旧 → 再插新”三步(多数场景走 delete + insert 路径):
- 第一步 insert 尝试时,已对唯一索引加
LOCK_X(不是普通 insert 的LOCK_S),且该锁不会因失败而立即释放 - 第二步检测到冲突后,需再次定位并锁定原记录(聚集索引 + 唯一索引),此时可能与另一个并发事务形成交叉持锁
- 第三步 insert 新记录时,若插入位置的下一条记录已被其他事务加锁,当前事务会尝试加
LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION—— 这个锁和已有LOCK_X行锁互斥,死锁闭环就此形成
on duplicate key update 的加锁行为更轻量
它只走“查→锁→改/插”单路径,且全程最多持两个行级锁(唯一索引行 + 对应聚集索引行),无回滚、无二次插入意向锁申请:
- 查唯一键存在时:对冲突行加
LOCK_X(仅一次,非两次) - 执行
UPDATE:复用已持有的LOCK_X,不新增锁 - 查唯一键不存在时:等效于普通
INSERT,只加必要行锁和间隙锁,不涉及 delete 阶段 - 没有 “先插失败再补救” 的中间状态,事务生命周期内锁持有时间更短、范围更确定
替换时要注意的兼容性细节
REPLACE INTO 和 INSERT ... ON DUPLICATE KEY UPDATE 的语义不完全等价,直接替换需检查以下三点:
-
REPLACE INTO返回受影响行数为 1(插入)或 2(删除+插入);而ON DUPLICATE KEY UPDATE插入返回 1,更新返回 2 —— 若业务依赖返回值判断是否发生了更新,需调整逻辑 - 如果表有多个唯一索引(如主键 + 多个
UNIQUE KEY),ON DUPLICATE KEY UPDATE只响应第一个触发冲突的索引;REPLACE INTO同样如此,这点行为一致,但需确认业务是否隐式依赖“哪个索引触发”的顺序 -
REPLACE INTO会清空未显式指定的列(设为默认值或 NULL);而ON DUPLICATE KEY UPDATE只更新UPDATE子句中列出的字段 —— 必须显式写出所有需保留/更新的列,漏写会导致字段被忽略而非重置
实际替换示例与验证要点
假设原语句是:
REPLACE INTO user_profile (uid, name, email, gtm_modify) VALUES (123, 'Alice', 'a@b.com', NOW());
应改为:
INSERT INTO user_profile (uid, name, email, gtm_modify) VALUES (123, 'Alice', 'a@b.com', NOW()) ON DUPLICATE KEY UPDATE name = VALUES(name), email = VALUES(email), gtm_modify = VALUES(gtm_modify);
上线前务必验证:
- 冲突场景下是否真能更新(而非报错或忽略)
- 非冲突场景下是否仍能正常插入(尤其注意自增主键是否被意外跳过)
- 事务中混用该语句时,是否与其他语句产生新的锁等待(例如先 select for update 再 on duplicate,可能引发 gap lock 扩展)
真正容易被忽略的是:即使换了语法,若业务本身在高并发下频繁更新同一组唯一键值(比如大量设备上报同个 device_id),仍可能因行锁争用造成延迟上升——这时锁竞争没消失,只是死锁没了。得结合业务节奏做削峰或分片,不能只靠 SQL 层替换一劳永逸。











