订单号不能用auto_increment主键,因其无法拼前缀、按天重置;触发器中max()+like有并发重复和性能问题;应使用独立seq_daily表配合select...for update;时区不一致会导致日期错乱,推荐应用层生成订单号并用唯一索引兜底。

订单号不能直接用 AUTO_INCREMENT 主键的底层原因
因为 AUTO_INCREMENT 生成的是纯数字、全局单调递增、且必须绑定主键或唯一索引列——它没法拼前缀(如 "ORD20260928"),也不能按天重置(如每天从 0001 开始)。硬塞进自增字段会直接报错:ERROR 1362 (HY000): Updating of AUTO_INCREMENT column is not allowed,或者触发器里读到的 NEW.id 是 0,导致拼出 "ORD202609280" 这种残缺编号。
BEFORE INSERT 触发器里查 MAX() + LIKE 的并发陷阱
常见错误写法是:在触发器里执行 SELECT MAX(order_no) FROM orders WHERE order_no LIKE "ORD20260928%",再加 1。但多个并发插入会同时查到同一个最大值,结果生成重复订单号。
- 大表上
MAX() + LIKE会全表扫描,慢且锁表 -
SELECT ... FOR UPDATE在触发器里无效(触发器不能开启/提交事务) - 正确做法是单独建一张
seq_daily表,每行存一个日期和计数器,用SELECT ... FOR UPDATE锁住当天那行再更新
时区不一致导致订单号日期错乱
NOW() 返回的是 MySQL 服务器当前时区的时间,不是客户端时间,也不是 UTC。如果你的应用用 UTC 时间下单,但数据库设了 default-time-zone='+08:00',就可能出现“用户 9 月 28 日 00:05 下单,订单号却是 ORD202609270001”。
- 统一用
CURDATE()而不是DATE(NOW()),语义更明确,不受时分秒干扰 - 检查
SELECT @@global.time_zone, @@session.time_zone,确保和业务预期一致 - 如果要按用户本地时间生成订单号,这事必须在应用层做——触发器看不到 HTTP 请求头或用户时区
为什么更推荐应用层生成 + 唯一索引兜底
触发器方案看似“自动”,实则隐藏复杂性:锁策略、时区、事务边界、性能瓶颈都得自己扛。而应用层生成订单号,配合 UNIQUE KEY (order_no),失败时重试或降级,逻辑更可控、可观测、可测试。
- 避免在数据库里做字符串拼接、日期格式化、并发计数等本该由业务代码承担的事
- 唯一索引能兜住绝大多数重复场景,比依赖触发器逻辑更可靠
- 订单号含业务语义(如渠道、地区、类型)时,只有应用层才能拿到完整上下文
真正容易被忽略的点是:触发器里的 CURDATE() 和应用层的 LocalDateTime.now() 可能在不同机器、不同时区、不同配置下返回不同日期——这个差异不会报错,只会悄悄造出跨日重复或跳号。











