如何运用SQL触发器检测并拦截重复的业务订单号提交?

作者:袖梨 2026-07-15
核心是用BEFORE INSERT触发器在插入前拦截,避免写入再回滚;但不可仅依赖SELECT查重防幻读,必须结合UNIQUE约束兜底,触发器仅用于抛出自定义错误或实现复杂业务规则。

触发器里怎么判断订单号是否已存在

核心是用 AFTER INSERTBEFORE INSERT 触发器查表,但必须避开自引用死锁和性能陷阱。推荐用 BEFORE INSERT —— 在插入真正发生前拦截,避免写入再回滚的开销。

常见错误是直接在触发器里写 SELECT COUNT(*) FROM orders WHERE order_no = NEW.order_no,这在高并发下可能漏判(幻读),尤其没加 FOR UPDATE 或事务隔离级别不够时。

  • MySQL 8.0+ 可配合 SELECT ... FOR SHARE(可读已提交下足够)
  • PostgreSQL 必须用 SELECT ... FOR NO KEY UPDATE 防止其他事务插入相同值
  • SQL Server 建议用 IF EXISTS (SELECT 1 FROM orders WITH (UPDLOCK, HOLDLOCK) WHERE order_no = @order_no)HOLDLOCK 等价于 SERIALIZABLE

为什么不能只靠唯一索引而要用触发器

唯一索引(UNIQUE(order_no))确实能拦住重复,但它抛出的是数据库级错误(如 MySQL 的 1062 Duplicate entry),业务层通常只能捕获通用异常,难以区分“重复订单”和“其他唯一键冲突”。触发器可以抛出自定义错误信息,比如 RAISE EXCEPTION '订单号 % 已存在', NEW.order_no(PostgreSQL)或 SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '重复订单号'(MySQL 5.5+)。

另外,有些场景需要更复杂的重复逻辑:比如“同一客户 24 小时内不能提交相同订单号”,这时唯一索引完全无能为力,必须靠触发器中写条件查询。

触发器里抛错后事务怎么回滚

关键点:触发器本身不开启事务,它运行在当前语句的事务上下文中。只要触发器抛出异常(SIGNAL / RAISE / THROW),整个 INSERT 语句就会失败,且自动回滚——前提是客户端没显式关闭自动提交或手动 COMMIT

  • MySQL 中 SIGNAL 是原子性的,无需额外 ROLLBACK
  • PostgreSQL 中 RAISE EXCEPTION 会立即终止当前函数并回滚当前语句
  • SQL Server 中 THROW 同样中断执行,但要注意 TRY...CATCH 是否包裹了外层逻辑,否则可能被吞掉错误

容易踩的坑:在存储过程中调用插入语句,又没检查返回状态,导致触发器报错但应用层以为成功。

性能和并发下的实际限制

触发器本质是行级锁 + 额外查询,每插一条都多一次查表。当订单表超千万行、QPS 过千时,BEFORE INSERT 触发器会成为瓶颈。此时应优先考虑:

  • order_no 字段设为 UNIQUE 索引(这是底线)
  • 应用层做幂等控制(如 Redis 记录 order_no:expire=300s
  • 触发器只用于兜底,而非主校验手段

另一个隐形问题:触发器无法拦截批量插入(INSERT ... SELECT)中的部分重复,MySQL 5.7+ 默认行为是整批失败,但 PostgreSQL 可能只报第一个冲突——得看具体版本和配置。

相关文章

精彩推荐