mysql中用insert on duplicate key update实现幂等写入,要求目标字段有primary key或unique索引,冲突时更新指定字段;postgresql则用on conflict do update,必须基于唯一约束并显式指定冲突目标。

直接捕获唯一约束异常比预查更可靠,因为“先查再插”存在竞态窗口、锁表风险和死锁隐患;生产环境应信任数据库的约束检查能力,用 INSERT ... ON DUPLICATE KEY UPDATE(MySQL)或 ON CONFLICT DO UPDATE(PostgreSQL)实现幂等写入。
MySQL 中用 ON DUPLICATE KEY UPDATE 处理冲突
该语法要求目标字段上有 PRIMARY KEY 或 UNIQUE 索引,触发条件是插入值与已有索引键冲突。
-
VALUES(column)引用的是本次INSERT中该列的原始值,不是当前行旧值 - 不要用
REPLACE INTO:它本质是DELETE + INSERT,会丢失自增 ID、触发DELETE钩子、破坏外键引用计数 -
INSERT IGNORE仅静默跳过,无法更新,且mysql_affected_rows()返回 0,无法区分“真没插入”和“因冲突被忽略”
示例:
INSERT INTO tb_user (id_card, username) VALUES ('370000199901016666','张三新名')
ON DUPLICATE KEY UPDATE username = VALUES(username), updated_at = NOW();
PostgreSQL 中必须用 ON CONFLICT DO UPDATE
PostgreSQL 不支持标准 SQL 的 MERGE,唯一合规的 UPSERT 方式是 ON CONFLICT。它必须显式指定冲突目标(索引名或列名),避免误匹配。
-
EXCLUDED是个伪表,代表本次想插入但被拒绝的那行数据 - 如果唯一约束是联合索引(如
(date, type)),必须写全列或明确索引名,不能只写ON CONFLICT (date) - 加
WHERE条件可实现精细逻辑,例如“仅当旧值为空时才覆盖”:DO UPDATE SET username = EXCLUDED.username WHERE tb_user.username IS NULL
示例(基于唯一索引 un_id_card):
INSERT INTO tb_user (id_card, username) VALUES ('370000199901016666','李四')
ON CONFLICT ON CONSTRAINT un_id_card
DO UPDATE SET username = EXCLUDED.username, updated_at = NOW();
Java 应用中精准捕获 SQLIntegrityConstraintViolationException
这个异常本身不直接暴露冲突字段名,需结合 SQLState 和错误消息定位:
- MySQL 报错码通常是
1062,SQLState为23000 - PostgreSQL 明确提示
DETAIL: Key (username)=(john) already exists.,可直接提取字段和值 - 在 JDBC URL 中添加
?loggerLevel=DEBUG(pgjdbc)或启用驱动日志,有助于还原完整上下文 - 不要只 catch
SQLException,应专门捕获SQLIntegrityConstraintViolationException并按约束名返回业务提示(如“手机号已被注册”)
Go 应用里要警惕 database/sql 的静默重试陷阱
Go 标准库在 Exec() 出现 driver.ErrBadConn 时会自动重试最多 10 次——而 lib/pq 在某些 SSL 异常下可能误报该错误,导致“第一次已成功插入,第二次重试触发唯一冲突”。
- 这不是应用层去重失效,而是基础设施层行为,
map[string]bool无法规避 - 禁用自动重试不可靠(
maxBadConnRetries是未导出字段),正确做法是改用事务 + 显式错误分类处理 - 收到
pq: duplicate key value violates unique constraint后,应检查是否是重试导致的误报,而非直接归因于业务逻辑重复
真正难处理的,是那种看起来“不可能发生”的冲突——比如单次请求、无并发、有本地哈希去重,却仍报唯一错误。这时候得往驱动层和连接复用机制里挖,而不是反复检查 SQL 语句。











