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