核心是用before insert触发器在插入前拦截,避免写入再回滚;但不可仅依赖select查重防幻读,必须结合unique约束兜底,触发器仅用于抛出自定义错误或实现复杂业务规则。

触发器里怎么判断订单号是否已存在
核心是用 AFTER INSERT 或 BEFORE 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 可能只报第一个冲突——得看具体版本和配置。











