主键冲突本质是innodb加锁机制导致的锁等待,而非单纯数据重复;insert ignore因不加x锁更抗并发,而on duplicate key update需严格保序防间隙锁死等。

主键冲突不是“撞上了”,是事务加锁机制在起作用
MySQL 报 Duplicate entry for key 'PRIMARY' 看似是数据重复,但高并发下更常见的现象是“卡住几秒后报错”或直接超时——这说明问题不在数据本身,而在锁等待。根本原因是 INSERT ... ON DUPLICATE KEY UPDATE 会在检查唯一性前就对目标行(或间隙)加 X lock 和 insert intention lock。多个事务同时插相同主键(比如订单号、设备 ID),第一个持锁,其余全卡在 lock_mode X locks gap before rec insert intention waiting 状态。
常见诱因包括:
- 应用层用本地时间戳+随机数生成 ID,毫秒内重复率高
- 消息重试未做幂等,同一事件被消费多次
- 分库分表后雪花 ID 生成器漂移,不同节点产出相近值
- 批量插入语句中
VALUES()内主键乱序,触发交叉间隙锁
INSERT IGNORE 为什么比 ON DUPLICATE KEY UPDATE 更抗并发
INSERT IGNORE 在检测到唯一键冲突时直接跳过,不加 X lock,不触发行更新,也不维护二级索引变更日志;而 ON DUPLICATE KEY UPDATE 即使 UPDATE 子句没真改字段(比如写成 id=id),也会对目标行加 X lock,并可能引发索引分裂。语义允许“冲突即丢弃”时,应无条件选 INSERT IGNORE。
使用要点:
- 用
ROW_COUNT()判断结果:返回1表示新增,0表示因冲突被忽略 - 不能靠它捕获其他错误(如字段类型不匹配),因为它对所有约束违规都静默忽略
- 建表时确认只有
PRIMARY KEY或明确的UNIQUE KEY,普通索引不会触发 ignore 行为
批量插入必须升序,否则间隙锁会互相死等
InnoDB 的间隙锁机制决定了:只要一批 INSERT VALUES () 中的主键值乱序,就大概率触发交叉加锁。例如插入 (100, 'a'), (99, 'b'), (101, 'c'),事务 A 锁了 99–100 间隙,事务 B 锁了 100–101 间隙,互相等待——这不是死锁,但会触发 Lock wait timeout exceeded。
实操要求:
- 应用层插入前必须
ORDER BY id ASC,不能依赖INSERT ... SELECT ... ORDER BY(它不影响间隙锁行为) - 单批不超过
500行;超量必须拆成多条语句,每条内部严格升序 - 若用雪花 ID,生成后立刻在内存排序,再拼 SQL;禁用
UUID()或MD5()直接作主键
REPLACE INTO 是伪 UPSERT,别在关键路径上用
REPLACE INTO 本质是 DELETE + INSERT,不是原子操作。它会破坏外键引用、触发 BEFORE/AFTER DELETE 触发器,还让 AUTO_INCREMENT 计数器跳号。在有 ON DELETE CASCADE 的表里,关联行会被连带删除;若业务依赖触发器做审计,你会看到一条 DELETE 日志 + 一条 INSERT 日志,语义完全失真。
更隐蔽的问题:
-
REPLACE INTO影响行数可能是2(新插)或3(删+插),而ON DUPLICATE KEY UPDATE是1或2,下游监控容易误判 - 它不满足幂等性:两次相同
REPLACE会导致自增 ID 增长两次 - PostgreSQL 和 SQL Server 根本没有这个语法,强行迁移成本高
ON DUPLICATE KEY UPDATE 当成廉价的幂等开关”。这两点一旦出错,压力一上来,锁等待就会从毫秒级迅速恶化成秒级甚至超时。











