触发器中不能用last_insert_id()获取新自增id,应直接使用new.id;并发流水号需用独立计数表+on duplicate key update实现原子递增,高并发场景推荐应用层或redis方案。

触发器里不能直接用 LAST_INSERT_ID() 获取新生成的自增 ID
很多人以为插入后在 AFTER INSERT 触发器里调用 LAST_INSERT_ID() 就能拿到刚插入的主键,但实际它返回的是「当前会话最后一次 INSERT 生成的 ID」——而触发器执行时,该语句尚未完成提交,LAST_INSERT_ID() 可能还是上一条语句的值,甚至为 0。
正确做法是显式引用新行字段:NEW.id(假设主键叫 id)。前提是表本身有自增主键,且你希望流水号基于它生成。
- 如果主键不是自增,或你想用其他字段做流水号基底,必须确保该字段在
INSERT语句中明确赋值 -
BEFORE INSERT触发器可以修改NEW.xxx,但无法读取自增 ID(因为还没生成) -
AFTER INSERT才能安全使用NEW.id,但注意:该触发器不能修改本行数据(MySQL 报错Can't update table 'xxx' in stored function/trigger)
流水号格式通常需拼接前缀 + 日期 + 序号,别硬编码进触发器
常见需求如 "ORD202405200001",其中 ORD 是业务前缀,20240520 是当天日期,0001 是当日序号。难点不在拼字符串,而在「当日序号」怎么安全递增。
直接在触发器里查最大值再加一(SELECT MAX(seq) FROM xxx WHERE DATE(create_time) = CURDATE())看似简单,但在并发插入时大概率重复——两个事务同时查到 0001,都写入 0002。
- 推荐用单独的流水号表 +
INSERT ... ON DUPLICATE KEY UPDATE或REPLACE INTO实现原子递增 - 流水号表结构示例:
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 = seq + 1,再查出新值 - 避免在触发器里做复杂查询或跨库操作,否则拖慢主表插入性能
触发器报错导致插入失败,务必测试回滚行为
触发器里任何 SQL 报错(比如插入流水号表失败、字段长度超限、子查询返回多行),都会让整个 INSERT 语句回滚——这符合预期,但容易被忽略的是:应用层可能只看到「插入失败」,却不知道根源在触发器。
调试时建议先关掉触发器(DROP TRIGGER IF EXISTS xxx),手动模拟触发器逻辑跑一遍,确认所有表存在、权限够、SQL 语法对。
- 检查触发器定义是否用了未授权的函数,如
SYS_GUID()在 MySQL 不可用,要用UUID()或CONV(FLOOR(RAND()*999999999999),10,36) - 若流水号依赖其他表,确保该表引擎是
InnoDB(支持事务),不要用MyISAM - 触发器中禁止调用非确定性函数如
NOW()做主键或唯一约束依据,否则复制环境可能不一致
更稳妥的替代方案:把流水号生成移到应用层或存储过程
纯靠触发器实现健壮流水号,在高并发、分库分表、需要跨服务协调的场景下,很快会遇到瓶颈。真正生产环境里,多数团队选择绕开触发器。
例如用 Redis 的 INCR 按日期 key 自增(INCR order:20240520),再拼前缀;或者用数据库序列(MySQL 8.0+ 支持 CREATE SEQUENCE)配合应用层获取。
- 触发器适合低频、单机、逻辑简单的场景,比如后台管理系统的日志编号
- 一旦涉及分布式、多实例、强一致性要求,触发器就成了黑盒隐患
- 哪怕坚持用触发器,也得预留降级开关——比如加个配置字段
auto_gen_sn,允许临时关闭流水号生成
最常被漏掉的一点:流水号往往要对外展示或用于对账,一旦生成就不能变更。所以无论用哪种方式,生成后必须立刻持久化、不可依赖内存或缓存,否则重启或故障就乱了。











