最稳妥的订单号生成方案是用单行计数表+for update锁或insert...on duplicate key update+last_insert_id(),确保日期与序号分离、原子递增、并发安全且每日重置。

直接用 NOW() 拼接 + 自增 ID 是最常见也最容易翻车的做法,根本不可用于生产环境。真正能落地的方案,必须把「日期维度」和「序号递增」拆开,用独立计数表 + 行锁(FOR UPDATE)或 INSERT ... ON DUPLICATE KEY UPDATE 保障原子性。
为什么不能在 BEFORE INSERT 触发器里用 NEW.id 和 DATE_FORMAT(NOW(), '%Y%m%d')
这是被抄烂却没人验证的错误范式:
-
NEW.id在BEFORE INSERT阶段始终是0或NULL,除非你手动赋值——但那就绕过了自增机制 -
NOW()在同一语句中多次调用(比如日志记录+拼号各一次),可能因毫秒级跳变返回两个不同日期字符串 - 若要求“每日重置”,用全局自增 ID 取模或截取后缀,很快会溢出或撞号
推荐方案:用单行计数表 + FOR UPDATE 锁
适用于固定前缀、日期+流水号格式(如 ORD202606060001),并发安全且逻辑清晰:
- 建表只存一行:
CREATE TABLE seq_generator (name VARCHAR(50) PRIMARY KEY, next_val BIGINT NOT NULL); - 插入初始值:
INSERT INTO seq_generator VALUES ('order_no', 1); - 存储过程中必须显式
START TRANSACTION,否则FOR UPDATE在自动提交模式下无效 - 关键格式化用
LPAD(cur_val, 4, '0'),不是cur_val + 10000这类取巧写法(会整型溢出)
高并发更稳的替代:用 INSERT ... ON DUPLICATE KEY UPDATE + LAST_INSERT_ID()
适合按日期分片计数(如每天一张记录),避免单行锁成为瓶颈:
- 建表:
CREATE TABLE seq_counter (seq_date DATE PRIMARY KEY, seq_value INT NOT NULL DEFAULT 0); - 核心语句:
INSERT INTO seq_counter (seq_date, seq_value) VALUES (CURDATE(), 1) ON DUPLICATE KEY UPDATE seq_value = LAST_INSERT_ID(seq_value + 1); - 立刻用
SELECT LAST_INSERT_ID() INTO @next_no;获取新值——这是 MySQL 唯一能保证原子递增且可回滚的方式 - 别用
SELECT MAX() + 1查再插,高并发下必然重复
调用时最容易被忽略的坑
生成号段和写入业务表必须包进**同一个事务**。如果先调存储过程拿到 @v_serial_number,再在外面 INSERT 业务记录,中间任何失败都会导致号空缺(已生成但未使用)或重号(重试时又取一次)。
另外,所有涉及计数更新的表操作,WHERE 条件必须走主键或唯一索引,否则 FOR UPDATE 会升级为表锁,引发死锁或性能雪崩。











