真正可靠的防重组合是索引+应用层重试+触发器收尾:底层用带业务语义的unique索引(如(user_id, date(created_at)))保障并发安全,中间层由应用捕获唯一冲突错误并退避重试,上层触发器仅执行轻量副作用操作。

UNIQUE约束本身就能防重复,为什么还要触发器?
因为 UNIQUE 只校验最终写入值,不感知业务语义。比如“同一用户当天只能提交一条订单”,这个“当天”是动态计算的,数据库原生约束无法表达时间范围逻辑。
触发器是在 INSERT 或 UPDATE 执行前/后介入的钩子,能跑自定义 SQL 判断,但要注意:它不能替代 UNIQUE 约束——否则并发时仍可能撞车。
- 必须先建
UNIQUE索引(哪怕组合字段含函数结果),这是并发安全的底层保障 - 触发器只做二次校验或补充逻辑(如写日志、改状态),不是防重主防线
- PostgreSQL 支持函数索引(
CREATE UNIQUE INDEX ON t ((user_id, DATE(created_at)))),MySQL 8.0+ 也支持,但老版本得用生成列
BEFORE INSERT 触发器里查表容易死锁
在触发器里执行 SELECT ... FOR UPDATE 或甚至普通 SELECT 查同一张表,高并发下极易引发行锁等待、死锁,尤其当事务还涉及其他表时。
更稳的做法是:把防重逻辑压到唯一索引 + 应用层重试,触发器只做副作用操作(比如更新统计字段)。
- 如果非要用触发器查表,务必用
SELECT ... LOCK IN SHARE MODE(MySQL)或显式SELECT FOR SHARE(PG),避免升级成排他锁 - 禁止在触发器里调用存储过程或外部服务,延迟不可控
- 触发器中
SELECT COUNT(*)没有索引支撑时,会全表扫,拖慢所有写入
MySQL 和 PostgreSQL 的触发器行为差异
MySQL 的 BEFORE INSERT 触发器可以修改 NEW 行数据(比如自动补 created_at),而 PostgreSQL 要求用 RETURN NEW 显式返回修改后的行,否则丢弃整行。
另外,PG 的触发器函数必须返回 TRIGGER 类型,MySQL 不强制;PG 支持 WHEN 条件子句,MySQL 5.7 不支持,8.0+ 才有类似能力。
- MySQL 中误写
SET NEW.id = UUID()在AFTER INSERT会静默失败,没报错但也不生效 - PG 中若触发器函数里漏了
RETURN NEW,插入直接报错:trigger function must return type "trigger" - 两者对
NULL值的UNIQUE索引处理一致:多行NULL不冲突,但业务上常要避免这种“假唯一”
真正可靠的防重组合:索引 + 应用层重试 + 触发器收尾
靠触发器单点拦截并发重复,就像用胶带绑住高压水管——看着管用了,其实压力全堆在别处。实际线上系统都是三层兜底:
- 最底下:带业务语义的
UNIQUE索引(例如(user_id, DATE(created_at))) - 中间层:应用捕获
1062 Duplicate entry(MySQL)或23505(PG)错误,退避后重试一次 - 最上层:触发器只干轻量事,比如更新
last_submit_time字段或写入审计表,不参与判断
很多人卡在“想让触发器包打天下”,结果一压测就超时。其实并发防重的复杂点不在逻辑多难写,而在锁粒度、索引覆盖、错误码识别这三处——漏一个,就等于没防。










