核心是用BEFORE INSERT触发器在插入前拦截,避免写入再回滚;但不可仅依赖SELECT查重防幻读,必须结合UNIQUE约束兜底,触发器仅用于抛出自定义错误或实现复杂业务规则。
核心是用 AFTER INSERT 或 BEFORE INSERT 触发器查表,但必须避开自引用死锁和性能陷阱。推荐用 BEFORE INSERT —— 在插入真正发生前拦截,避免写入再回滚的开销。
常见错误是直接在触发器里写 SELECT COUNT(*) FROM orders WHERE order_no = NEW.order_no,这在高并发下可能漏判(幻读),尤其没加 FOR UPDATE 或事务隔离级别不够时。
SELECT ... FOR SHARE(可读已提交下足够)SELECT ... FOR NO KEY UPDATE 防止其他事务插入相同值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。
SIGNAL 是原子性的,无需额外 ROLLBACK
RAISE EXCEPTION 会立即终止当前函数并回滚当前语句THROW 同样中断执行,但要注意 TRY...CATCH 是否包裹了外层逻辑,否则可能被吞掉错误容易踩的坑:在存储过程中调用插入语句,又没检查返回状态,导致触发器报错但应用层以为成功。
触发器本质是行级锁 + 额外查询,每插一条都多一次查表。当订单表超千万行、QPS 过千时,BEFORE INSERT 触发器会成为瓶颈。此时应优先考虑:
order_no 字段设为 UNIQUE 索引(这是底线)order_no:expire=300s)另一个隐形问题:触发器无法拦截批量插入(INSERT ... SELECT)中的部分重复,MySQL 5.7+ 默认行为是整批失败,但 PostgreSQL 可能只报第一个冲突——得看具体版本和配置。