MySQL触发器不能用AUTO_INCREMENT实现业务流水号,因其为表级全局递增,而业务号需带前缀、按日期分段重置且严格连续;并发下MAX()+1易重复或锁表;应使用独立计数表配合SELECT...FOR UPDATE行锁保证安全。

MySQL触发器里不能直接用 AUTO_INCREMENT 实现业务流水号
因为 AUTO_INCREMENT 是表级、全局递增的整数,而业务流水号通常需要带前缀(如 "ORD202405")、按日期/部门分段重置、且必须严格连续不跳号。触发器里如果只靠 MAX()+1 查最大值再加一,在并发插入时大概率重复或锁表卡死。
用独立计数表 + SELECT ... FOR UPDATE 是最稳的方案
核心思路是把“取号”动作从主表解耦出来,用一张专用计数表控制号段,靠行锁保证并发安全。示例场景:订单号格式为 "ORD" + YYYYMM + 4位流水(如 "ORD2024050001"):
- 建一张
seq_counter表:CREATE TABLE seq_counter ( prefix VARCHAR(10) NOT NULL, yyyymm CHAR(6) NOT NULL, next_val INT UNSIGNED NOT NULL DEFAULT 1, PRIMARY KEY (prefix, yyyymm) );
- 在触发器中先查并锁住当天号段:
SELECT next_val INTO @cur_val FROM seq_counter WHERE prefix = 'ORD' AND yyyymm = DATE_FORMAT(NOW(), '%Y%m') FOR UPDATE;
- 若没查到,INSERT 一行并设
next_val = 1;查到了就更新:UPDATE seq_counter SET next_val = next_val + 1 WHERE prefix = 'ORD' AND yyyymm = DATE_FORMAT(NOW(), '%Y%m');
- 拼接最终流水号:
CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m'), LPAD(@cur_val, 4, '0'))
注意 INSERT ... ON DUPLICATE KEY UPDATE 会绕过 FOR UPDATE 锁
有人图省事想用 INSERT ... ON DUPLICATE KEY UPDATE 替代 SELECT ... FOR UPDATE + UPDATE,但这是错的——ON DUPLICATE KEY UPDATE 在冲突时不会等待锁,而是直接报错或跳过,导致并发下漏号或重复号。必须显式用 SELECT ... FOR UPDATE 抢锁,哪怕多一次查询。
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
触发器里不能调用存储函数返回 UUID 或 NOW() 做唯一性兜底
流水号是业务强约束,不是防重复的补救手段。用 UUID() 或 NOW() 拼接会导致格式不可读、无法排序、审计困难。更关键的是:触发器中调用 NOW() 在事务回滚后,计数表已更新,但主表没写入,这个号就永久丢失,造成空洞。真正要保“连续”,就得接受“号段预占+定期对账”的运维成本。










