insert ignore在高并发下不一定防重,因其“检查→插入”非原子操作,存在竞态窗口;on duplicate key update更稳妥,通过next-key lock串行化处理;replace into易引发删除副作用;业务层需配合幂等校验与合理唯一索引设计。

INSERT IGNORE 为什么在高并发下不一定防重
它只是跳过重复键错误,但前提是冲突发生在唯一索引检查阶段;如果两个事务几乎同时执行 INSERT IGNORE,且都还没写入数据,就可能双双通过唯一性校验,最终都插入成功——特别是当唯一约束字段不是主键、或存在延迟提交时。
常见错误现象:Duplicate entry 'xxx' for key 'uk_name' 没报出来,但表里却出现了两条相同记录。
- 本质是「检查 → 插入」非原子操作,中间有窗口期
- 只对已存在的唯一索引生效,对未提交的同行记录不可见(RR 隔离级别下)
- 不阻塞其他事务,所以并发越高,漏插概率越大
ON DUPLICATE KEY UPDATE 是更稳妥的选择
它把「查+插/更新」合并成一条语句,在唯一索引冲突时直接走更新分支,避免了竞态窗口。MySQL 内部会为冲突的索引值加 next-key lock,锁住对应间隙和记录,强制串行化处理。
使用场景:用户注册、订单号生成、幂等写入接口。
- 必须配合唯一索引(主键或
UNIQUE约束),否则不触发更新逻辑 - 更新字段不能是被冲突的唯一键本身(比如
ON DUPLICATE KEY UPDATE id = id + 1会报错) - 性能影响:锁粒度比单纯 INSERT 大,但比 SELECT + INSERT 组合小得多
示例:INSERT INTO users (uid, name) VALUES (123, 'Alice') ON DUPLICATE KEY UPDATE name = VALUES(name);
REPLACE INTO 的锁行为容易引发意外删除
它本质是「DELETE + INSERT」,一旦命中唯一索引,就会先删旧行再插新行。这会导致:自增 ID 跳变、外键级联动作触发、触发器重复执行、以及更长的锁持有时间。
常见错误现象:明明只想更新一条记录,结果发现 id 变了,或者关联表数据被误删。
- 在 RR 隔离级别下,
REPLACE会加next-key lock锁住整个冲突范围,阻塞更多并发 - 如果唯一索引包含多个字段,且部分字段为 NULL,行为可能不符合预期(NULL 不参与唯一性比较)
- 不推荐用于有外键或复杂触发器的表
真正需要防重时,别只靠 SQL 层
MySQL 的锁机制再强,也解决不了应用层重复请求的问题。比如前端连点两次、Nginx 重试、消息队列重复投递——这些都会让同一条逻辑请求打到数据库多次。
这时候唯一索引 + ON DUPLICATE KEY UPDATE 只是兜底,不是主力防线。
- 业务层必须加幂等 key(如请求 ID、业务单号),并用 Redis 做前置校验
- 数据库唯一索引字段要覆盖所有防重维度,别漏掉状态字段(例如
order_no + status联合唯一) - 注意字符集和排序规则:utf8mb4_0900_as_cs 和 utf8mb4_unicode_ci 对大小写/空格的处理不同,可能导致「看似重复实则没锁住」
最常被忽略的一点:唯一索引失效不报错。比如字段定义为 VARCHAR(255) 但实际存了超长截断值,或者用了前缀索引而查询条件没命中前缀——这时根本不会触发唯一约束检查。










