唯一约束是订单号去重的正确方案,触发器仅适合格式校验等轻量预检,不可用于SELECT...FOR UPDATE查重或手动阻断并发插入。

触发器里不能用 SELECT ... FOR UPDATE
很多人想在 INSERT 触发器里先查一遍订单号是否存在,再加锁防止并发插入——这行不通。MySQL 的 BEFORE INSERT 触发器运行在语句执行前,但事务尚未真正持锁,且触发器内不支持 SELECT ... FOR UPDATE(会报错 ER_XAER_RMFAIL 或直接拒绝)。更关键的是,即使能加锁,多个会话同时进入触发器仍可能查到“不存在”,然后一起插入。
真正可靠的方案是把校验下推到存储引擎层:
- 给订单号字段加
UNIQUE约束(最简单、最高效) - 如果业务要求返回更友好的错误信息(比如“该订单已存在”而非“Duplicate entry”),再用触发器捕获冲突并抛出自定义提示
- 不要试图在触发器里手动查+判+阻断——逻辑不可靠,还拖慢性能
BEFORE INSERT 触发器只能做轻量预检
BEFORE INSERT 触发器适合做格式校验、默认值填充或简单逻辑拦截,比如检查 order_no 是否为空、是否符合正则模式、是否超出长度。但它无法安全完成“是否存在”的最终判定。
示例:只做基础过滤,不碰唯一性判断
CREATE TRIGGER check_order_no_format BEFORE INSERT ON orders FOR EACH ROW BEGIN IF NEW.order_no IS NULL OR LENGTH(NEW.order_no) <p>注意:<code>SIGNAL</code> 只能中断当前语句,不能替代约束;一旦 <code>UNIQUE</code> 冲突发生,它会在触发器之后才报错,所以触发器里的 <code>SIGNAL</code> 和约束冲突是两套独立机制。</p><h3>唯一约束 + 应用层重试才是生产级解法</h3><p>数据库层面靠 <code>UNIQUE KEY (order_no)</code> 拦住 99.9% 的并发重复,应用层负责兜底和用户体验。这是 MySQL 官方推荐的并发控制组合。</p><p>实际处理建议:</p>
- 建表时必须有
UNIQUE(order_no),哪怕 order_no 是业务生成的(如时间戳+随机数) - 应用代码捕获
1062 Duplicate entry错误码,而不是解析错误消息文本(因语言/版本可能变化) - 若需幂等创建,可改用
INSERT IGNORE或ON DUPLICATE KEY UPDATE,但要确认语义是否符合业务(比如是否允许静默忽略) - 避免在触发器里调用存储过程做远程校验(如查 Redis 或调 API),网络延迟和失败会放大事务风险
为什么不用 AFTER INSERT + 回滚?
有人想在 AFTER INSERT 里查重,发现重复就 ROLLBACK——MySQL 不允许在触发器里执行 ROLLBACK(会报 ER_NO_RAID 类错误)。而且 AFTER 阶段数据已写入,回滚代价高,还可能影响主从复制一致性。
真正需要复杂校验的场景(比如“同一用户 5 分钟内不能下相同商品的单”),应该:
- 用带
FOR UPDATE的SELECT在应用事务中前置锁定用户行或商品行 - 或引入分布式锁(如 Redis SETNX)配合 TTL 控制窗口期
- 触发器不是为此设计的,硬塞进去只会让问题更隐蔽
唯一性校验这件事,数据库约束就是答案。其他所有绕路方案,要么错,要么慢,要么难维护。










