不能靠new.id拼接日期加序号生成可靠流水号,因mysql 5.7触发器中before insert阶段new.id不可用、after insert不可修改字段,且并发下select max()+1必然重复;唯一可行方案是用独立计数表配合insert...on duplicate key update与last_insert_id()实现原子递增。

不能靠 NEW.id 拼接日期再加序号来生成可靠流水号,MySQL 5.7 的触发器机制决定了这种写法在并发或跨秒场景下必然出错。
BEFORE INSERT 中 NEW.id 是无效的
很多人直接写 SET NEW.sn = CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(NEW.id, 4, '0')),但这是错的——NEW.id 在 BEFORE INSERT 阶段还没生成,值为 0 或 NULL(取决于表定义),拼出来全是 ORD202607010000 这种固定值。
- 只有
AFTER INSERT才能安全读取NEW.id,但此时不能再修改本行字段(MySQL 报错Can't update table 'xxx' in stored function/trigger) - 若主键非自增,又没在 INSERT 语句中显式赋值,
NEW.id始终不可用 - 想绕过自增、手动赋值
NEW.id,等于放弃自增机制,需自行保证唯一性
并发下 SELECT MAX() + 1 必然重复
用 SELECT MAX(seq) FROM orders WHERE DATE(create_time) = CURDATE() 算当日最大序号,再加一,看似简单,但两个事务几乎同时执行时,都会查到同一个最大值,然后都写入下一个序号,导致重复。
- 哪怕加了索引,也解决不了幻读问题;
SELECT ... FOR UPDATE在触发器里无法对主表加锁(会报错) - MySQL 5.7 不支持在触发器里开启新事务,所以不能靠锁住计数表再查再更新的常规套路
- 真正可用的是原子写:用
INSERT ... ON DUPLICATE KEY UPDATE配合LAST_INSERT_ID()
必须用独立计数表 + LAST_INSERT_ID() 实现原子递增
建一张轻量级计数表,按日期做主键,用 INSERT ... ON DUPLICATE KEY UPDATE 触发 LAST_INSERT_ID() 返回新值,这是 MySQL 5.7 唯一能落地的原子方案。
- 建表:
CREATE TABLE seq_counter (date_key CHAR(8) PRIMARY KEY, seq INT DEFAULT 0) - 触发器内写:
INSERT INTO seq_counter VALUES (DATE_FORMAT(NOW(), '%Y%m%d'), 1) ON DUPLICATE KEY UPDATE seq = LAST_INSERT_ID(seq + 1) - 紧接着:
SELECT LAST_INSERT_ID() INTO @next_seq,再拼流水号:CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m%d'), LPAD(@next_seq, 4, '0')) - 注意:
LAST_INSERT_ID()是会话级函数,必须紧跟在INSERT ... ON DUPLICATE KEY UPDATE后调用,中间不能有其他影响它的语句
触发器报错会让整个 INSERT 回滚,但错误根源难定位
流水号表不存在、权限不足、字段超长、子查询返回多行——任何一点出错,都会让原始 INSERT 失败,但应用层只看到“插入失败”,不会提示是触发器里的哪条 SQL 挂了。
- 上线前务必关掉触发器,手动跑一遍触发逻辑,验证所有依赖表存在、字段类型匹配、SQL 语法无误
- 避免在触发器里调用未授权函数(如
SYS_GUID()),MySQL 5.7 只认UUID()、NOW()、DATE_FORMAT()等标准函数 - 不要在触发器里做跨库操作,除非已显式授权且目标库名硬编码(动态拼库名会报错)
真正麻烦的不是拼字符串,而是确保“当天序号”这个数字在并发下不撞车——MySQL 5.7 的触发器能力有限,能靠的只有 ON DUPLICATE KEY UPDATE 和 LAST_INSERT_ID() 这一对组合,其它都是障眼法。











