触发器中不能用uuid()或自增主键直接生成业务流水号,因uuid格式不符(如ord202405200001)、自增id不连续且无法控制格式;可靠方案是before insert触发器结合独立计数表seq_counter,通过select...for update锁住当日记录并原子更新next_val,再拼接前缀、日期与lpad格式化序号。

触发器里不能用 UUID() 或自增主键直接生成流水号
UUID() 生成的是 32 位十六进制字符串,不符合「业务流水号」常见格式(如 ORD202405200001);而自增主键 auto_increment 无法控制格式、跨表不连续、且在事务回滚后会产生空号。真正可用的方案是:在插入前,用当前日期 + 一个当天内递增的序号拼接,序号必须从数据库中实时查出并+1。
关键点在于:这个“当天最大序号”必须原子性地读取并更新,否则并发插入会撞号。MySQL 的 INSERT ... SELECT ... FOR UPDATE 或 INSERT ... ON DUPLICATE KEY UPDATE 配合唯一约束更可靠,但触发器里没法直接做“先查再插再更新”的完整闭环——所以得靠一张专门的流水号计数表 + 行级锁。
- 建一张
seq_counter表,字段至少含date_key(如'20240520')、next_val(默认 1) - 在触发器里用
SELECT ... FOR UPDATE锁住当天那行,再UPDATE它 - 避免用
LAST_INSERT_ID()模拟自增,它只对INSERT生效,且不保证并发安全
BEFORE INSERT 触发器中如何安全读写计数表
必须用 BEFORE INSERT,否则无法修改即将插入的记录字段;且不能在触发器里调用存储过程做复杂逻辑(部分 MySQL 版本限制或引发错误),所有操作要内联写死。
典型写法是:先 SELECT next_val INTO @cur_seq FROM seq_counter WHERE date_key = DATE_FORMAT(NOW(), '%Y%m%d') FOR UPDATE;如果查不到,就 INSERT 一行再重查;然后 SET NEW.order_no = CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(@cur_seq, 4, '0'));最后 UPDATE seq_counter SET next_val = next_val + 1 WHERE date_key = DATE_FORMAT(NOW(), '%Y%m%d')。
MySQL 9.6.0是面向Linux平台的2026年创新版本,核心架构迎来重大革新。其将外键约束与级联操作从InnoDB引擎层上移至SQL层,确保所有数据变更均被完整记录至Binlog,彻底解决了CDC(变更数据捕获)与主从复制中的数据不一致难题。此外,该版本引入container_aware启动选项以原生适配容器环境,并对审计日志进行了组件化重构,为追求极致数据一致性与云原生体验的开发者提供了全新选择。
- 注意
FOR UPDATE必须在事务上下文中才生效,所以触发器外的插入语句不能是 autocommit=1 的单条语句,否则锁立刻释放,失去意义 -
LPAD(@cur_seq, 4, '0')控制序号为 4 位,超出 9999 会截断,需提前加校验或改用 5 位 - 不要用
@@autocommit在触发器里临时关开关——不可靠,且高版本 MySQL 禁止在触发器中修改系统变量
并发插入时序号重复的三个典型原因
流水号重复几乎都来自并发场景下锁粒度或事务隔离问题,不是语法写错。
- 没用
SELECT ... FOR UPDATE,而是用普通SELECT+ 应用层计算,导致两个连接读到同一个next_val - 计数表缺少
UNIQUE KEY(date_key),导致重复INSERT插入多行相同日期,后续查询随机命中其中一行 - 触发器里用了
INSERT IGNORE或REPLACE INTO初始化计数行,但没配合FOR UPDATE,初始化和读取之间存在竞态窗口
验证是否真并发撞号:用 mysql -e "INSERT INTO orders () VALUES ();" & 跑 10 个并发,查 order_no 是否有重复——比看日志更快定位问题。
流水号格式变更后如何平滑迁移
业务上线后常要调整前缀(如 ORD → SO)或长度(4 位序号 → 5 位),不能直接改触发器,否则历史数据和新号规则混在一起,下游系统解析会出错。
- 新增字段存新流水号(如
order_no_v2),触发器同时填充两列,过渡期双写 - 计数表加
version字段区分规则,比如WHERE date_key = '20240520' AND version = 2 - 旧触发器保留不动,新建
orders_v2表配新触发器,用应用路由决定写哪张表
最易忽略的是:日期切换边界(如午夜 00:00)时,多个连接可能同时去初始化新日期的计数行,哪怕加了唯一索引,INSERT ... ON DUPLICATE KEY UPDATE 也比单纯 INSERT 更稳——但触发器里不能用这个语法,得靠外面包一层存储过程。










