在MySQL中,Replace Into 语句相比 Insert 会额外添加一把 LOCK_X 记录锁,原因在于其对唯一索引检查阶段直接施加排他锁,而 Insert 仅加共享锁。当发生冲突时,Replace Into 不会立即释放锁,而是先删除再插入,这使得锁持有时间更长、范围更广,更容易引发死锁。

来看一个核心差异:同样面临唯一键冲突时,普通 Insert 只在检查阶段加一把共享锁,而 Replace Into 一开始就加排他锁。这个差异直接决定了后续锁冲突概率和死锁风险的高低。
Replace Into 为何比 Insert 多加一把 LOCK_X 记录锁
根本原因在于:Replace Into 在唯一索引检查阶段就强制加上了排他锁(LOCK_X),而普通 Insert 仅加共享锁(LOCK_S)。这个细微差别实际上显著放大了锁冲突概率。
具体而言,当出现唯一键(UK)冲突时,InnoDB 在检查 b=8 是否已存在时,Insert 会加 LOCK_S | LOCK_REC_NOT_GAP,允许其他事务并发读取;而 Replace Into 强制加 LOCK_X,禁止其他任何事务对该记录加锁——即便是只读操作也不允许。
- Insert 检查冲突时加
LOCK_S,一旦冲突就报错退出,锁立即释放 - Replace Into 检查冲突时加
LOCK_X,冲突后不立即释放,而是回滚聚集索引插入,再执行 delete 和 insert,全程持有该LOCK_X - 若多个 Replace Into 并发争抢同一唯一值(例如都插入
b=8),第一个成功获取LOCK_X,其余全部等待,极易引发死锁
Replace Into 的两阶段加锁机制导致锁持有时间过长
Replace Into 并非原子操作,它被拆分为“先试插→失败→再删再插”两个阶段,并且第一阶段的锁不会回退,必须等到整个语句执行完毕才释放。
这意味着,即使你只想更新一条已存在的记录,MySQL 也会先尝试插入一行新记录(附带自增 ID),该操作会申请插入意向锁(LOCK_INSERT_INTENTION)和间隙锁(LOCK_GAP),而这些锁在冲突发生后不会立即释放,而是延续到后续的 delete 和 insert 完成。
- Session1 执行
replace into t(b) values (8),在b=8上加LOCK_X,同时在(8,100)区间加上 next-key 锁 - Session2 同样执行,卡在等待
LOCK_X,但它自身已持有部分 gap 锁 - 双方互相等待释放锁,最终形成死锁
Replace Into 删除旧记录会触发更多锁类型
Insert 只涉及插入路径,而 Replace Into 在冲突时必须走 delete 流程,这会触发更多锁行为,包括删除聚集索引行、标记二级索引项、清理 undo log 等,每一步都可能申请新锁。
尤其是当表包含多个唯一索引时,Replace Into 需要依次检查每个唯一键,只要有一个冲突,就可能在多个索引上叠加 LOCK_X;而 Insert 仅在报错前检查一次,并且只锁定命中的索引。
- 当存在联合唯一索引
UNIQUE KEY (a,b)和单列UNIQUE KEY (c)时,Replace Into 可能同时在两个位置加LOCK_X - Delete 操作本身会获取聚集索引记录上的
LOCK_X,并且可能触发外键检查锁(如果存在关联外键) - 自增列(
AUTO_INCREMENT)在 Replace Into 中仍然会分配新值(即使最终未使用),受innodb_autoinc_lock_mode参数影响,可能引入表级锁竞争
高并发下 Replace Into 更容易暴露锁粒度问题
Insert 是纯粹的新增操作,只要不碰撞唯一键,基本只锁定自身那一行;而 Replace Into 的语义本质是“覆盖”,其锁范围天然更宽——既要防止其他事务插入相同唯一值(gap lock),又要阻止他人修改或删除当前冲突行(record lock),还要为后续的 delete 和 insert 预留空间(insert intention lock)。
这种组合使得 Replace Into 在热点唯一键(如订单号、设备 ID、用户手机号)场景下极易导致锁等待链拉长。特别是当应用层没有控制并发量,也未添加重试机制时,一条失败的 Replace Into 可能阻塞后续数十个请求。
- 当并发量超过 50 QPS 且唯一键重复率超过 20% 时,
Deadlock found when trying to get lock错误出现频率显著增加 - 改用
INSERT ... ON DUPLICATE KEY UPDATE后,相同场景下死锁几乎消失,因为它执行原地 update,不会触发 delete 阶段的锁扩张 - 如果必须使用 Replace Into,应确保表只有一个唯一索引,并关闭外键、禁用 delete 触发器,否则锁路径会变得更加复杂
实际线上故障时,往往不是单条语句执行慢,而是某条 Replace Into 被卡住后拖垮整个连接池——这一点比锁本身更隐蔽。
