直接捕获唯一约束异常比预查更可靠,因预查存在竞态窗口、锁表风险及死锁隐患,而直接INSERT配合捕获1062错误码或ON DUPLICATE KEY UPDATE等机制可实现高效幂等写入。

直接捕获唯一约束异常比预查更可靠
预查(先 SELECT 再 INSERT)看似直观,但天然存在竞态窗口:两次请求查完都没数据,却在插入前被对方抢先写入。哪怕加了事务,也得配合 SELECT ... FOR UPDATE,且必须命中唯一索引才能锁住对应行——否则可能锁表或锁不住。高并发下还容易引发锁等待甚至死锁。
更稳的做法是信任数据库的约束检查能力,直接执行 INSERT,在应用层捕获具体错误码:
- MySQL:捕获错误号
1062或 SQLSTATE'23000' - PostgreSQL:捕获 SQLSTATE
'23505' - Dameng(达梦):捕获异常信息含
Violate unique constraint - Oracle:捕获
ORA-00001,用DUP_VAL_ON_INDEX异常名处理
捕获后不硬抛错,而是走业务逻辑分支——这才是生产环境的主流做法。
MySQL 用 ON DUPLICATE KEY UPDATE 实现幂等写入
这是 MySQL 最常用、语义最清晰的冲突处理方式。它要求目标字段上有 PRIMARY KEY 或 UNIQUE 索引,触发条件是插入值与已有索引键冲突。
示例:
INSERT INTO tb_user (id_card, username)
VALUES ('370000199901016666', '张三新名')
ON DUPLICATE KEY UPDATE username = VALUES(username), updated_at = NOW();
注意点:
-
VALUES(column)引用的是本次 INSERT 中该列的原始值,不是当前行旧值 - 不要滥用
REPLACE INTO:它本质是 DELETE + INSERT,会丢失自增 ID、触发 DELETE 钩子、破坏外键引用计数 -
INSERT IGNORE仅静默跳过,无法更新,也不返回影响行数(mysql_affected_rows()为 0),不利于判断是否真发生了冲突
PostgreSQL 必须用 ON CONFLICT DO UPDATE 而非 MERGE
PostgreSQL 不支持标准 SQL 的 MERGE,唯一合规的 UPSERT 方式是 ON CONFLICT。它的语法更显式,必须指定冲突目标(索引名或列名),避免误匹配。
示例(基于唯一索引 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();
关键细节:
-
EXCLUDED是个伪表,代表本次想插入但被拒绝的那行数据 - 如果唯一约束是联合索引(如
(date, type)),必须写全列或明确索引名,不能只写ON CONFLICT (date) - 不加
WHERE条件时,DO UPDATE总会执行;加了WHERE可实现“仅当旧值为空时才覆盖”等精细逻辑
别忽略触发器和同步链路带来的隐性冲突
有时你确认表里没重复数据,但插入仍报 Violate unique constraint on [my_table][load_uidxe_idx],查表也确实没有冲突记录——大概率是触发器在捣鬼。
典型场景:
- A 库插入触发器,自动同步数据到 B 库同名表
- B 库该表已有
date='2025-03-03' AND type=2,而 A 库没这行 - A 库插入时,触发器试图往 B 库插相同组合 → B 库报唯一冲突 → 触发器回滚 → A 库主事务也被拖垮
排查路径:
- 看报错中的索引名(如
load_uidxe_idx),反查该索引定义在哪张表、哪些字段 - 查
INFORMATION_SCHEMA.TRIGGERS或数据库管理界面,确认是否有涉及该表的触发器 - 临时禁用触发器重试,验证是否恢复;若恢复,问题就定位到同步逻辑
真正难缠的从来不是 INSERT 本身,而是约束背后那些看不见的联动动作。

















