靠唯一约束+insert ignore或on duplicate key update才能实现幂等;无唯一索引的insert必然不幂等,因无法识别业务重复性,易致数据重复或覆盖错误。

直接结论:靠唯一约束 + INSERT IGNORE 或 ON DUPLICATE KEY UPDATE,缺一不可;不建唯一索引的 INSERT 永远不幂等。
为什么 INSERT 默认不幂等?
普通 INSERT 语句没有识别“这是同一笔业务”的能力。比如两次插入 INSERT INTO orders (order_id, amount) VALUES ('ORD-001', 99.9),若 order_id 没建唯一索引,就会产生两条重复订单——这不是“重复执行”,是“重复写入”,结果完全不可控。
常见错误现象:
- 用
SELECT ... THEN INSERT判断是否存在,高并发下两个请求同时查不到,同时插入,冲突爆发 - 只依赖应用层生成 UUID 当主键,但没在数据库加唯一约束,UUID 再唯一也拦不住重复插入
- 把时间戳、自增 ID、随机数当幂等依据,它们无法表达业务语义,也无法防重
INSERT IGNORE 适用场景与陷阱
适合“只新增、不更新”的轻量写入,例如日志、事件快照、通知记录。
实操要点:
- 必须提前在表上对业务关键字段建
UNIQUE KEY,例如ALTER TABLE orders ADD UNIQUE (order_id) -
INSERT IGNORE遇到唯一键冲突时静默跳过,mysql_affected_rows()返回 0,不是报错 - 它不做任何更新——哪怕你希望每次写入都刷新
created_at或updated_at,它也完全不管 - MySQL 8.0.19+ 支持标准
INSERT ... ON CONFLICT DO NOTHING(PostgreSQL 风格),语义更明确
ON DUPLICATE KEY UPDATE 怎么写才安全?
适合需要“存在则覆盖部分字段”的业务,如订单状态更新、用户资料同步。
典型写法示例:
INSERT INTO orders (order_id, status, updated_at)
VALUES ('ORD-001', 'paid', NOW())
ON DUPLICATE KEY UPDATE
status = VALUES(status),
updated_at = NOW();
关键注意点:
-
VALUES(status)引用的是本次INSERT语句中提供的值,不是原记录值 - 不要无条件覆盖创建时间:
created_at = NOW()会把原始下单时间冲掉,应改用created_at = COALESCE(created_at, NOW()) - 避免覆盖业务敏感字段,比如金额、数量类字段,除非明确设计为“以最后为准”
- 该语句在 MySQL 中会触发
AUTO_INCREMENT值消耗(即使跳过插入),高并发下可能导致 ID 不连续
唯一索引建在哪?别只盯 id
主键 id 是技术主键,不是业务主键。幂等依赖的是业务唯一性,不是数据库自增规则。
应根据实际业务组合建唯一索引,例如:
- 支付单:
(biz_type, out_trade_no),区分充值/退款 + 外部单号 - 用户行为日志:
(user_id, event_type, DATE(event_time)),按天去重同类事件 - 配置项:
(app_name, env, key_name),三者共同决定一条配置
历史数据不洁?字段太长?可退一步用防重表(dedup_log),结构为 (biz_type, biz_id, created_at),其中 (biz_type, biz_id) 加唯一索引,写入前先 INSERT IGNORE 到该表,成功再走主逻辑——但必须和主事务强绑定(同事务 or XA),否则有窗口期。
最常被忽略的一点:唯一索引本身要能承载业务含义。如果一个订单可能多次发起支付(不同渠道、不同时间),那单用 order_id 就不够,得带上 pay_channel 或 pay_seq 才算真正唯一。











