触发器中不能用LAST_INSERT_ID()获取业务流水号,应使用独立流水号表配合SELECT...FOR UPDATE加锁+UPDATE实现;必须用BEFORE INSERT触发器赋值NEW字段,且需确保字段类型、约束匹配,避免非事务操作。

触发器里不能用 LAST_INSERT_ID() 获取新流水号
业务流水号通常要求“按规则自增、带前缀、不跳号”,但 MySQL 触发器执行时,INSERT 尚未提交,LAST_INSERT_ID() 还没被设置,直接调用会返回 0 或上一条连接的值。更麻烦的是,并发插入时靠它生成流水号必然冲突或重复。
真正可行的做法是:用一张独立的流水号表 + SELECT ... FOR UPDATE 锁定行,再更新并读取新值。触发器里不能开事务,但 InnoDB 支持在触发器中执行带锁的 SELECT(前提是存储引擎支持行锁)。
- 建一张专用表:
CREATE TABLE seq_generator (name VARCHAR(32) PRIMARY KEY, next_val BIGINT NOT NULL); - 初始化记录:
INSERT INTO seq_generator VALUES ('order_no', 100001); - 触发器内用
SELECT next_val FROM seq_generator WHERE name = 'order_no' FOR UPDATE;拿到当前值,再UPDATE seq_generator SET next_val = next_val + 1 WHERE name = 'order_no';
触发器必须定义为 BEFORE INSERT,且不能用 AFTER
AFTER INSERT 触发器无法修改正在插入的那行数据——NEW 已只读,赋值会报错 Can't update table in stored function/trigger。只有 BEFORE INSERT 才允许给 NEW.xxx 赋值,把生成的流水号塞进去。
另外注意:如果表有多个唯一约束或外键依赖流水号字段,务必确保触发器生成的值满足所有校验,否则会卡在约束检查阶段,报错如 Column 'order_no' cannot be null 或违反 UNIQUE。
- 字段类型要匹配:比如流水号是
VARCHAR,就别往里塞数字;若定义了NOT NULL,触发器必须赋值 - 避免在触发器里调用函数做复杂拼接(如日期+序列),容易拖慢写入;简单字符串拼接用
CONCAT('ORD-', LPAD(next_val, 6, '0'))即可 - 不要在触发器里写
INSERT INTO ... SELECT或调用存储过程——多数 MySQL 版本禁止这类操作
并发场景下 SELECT ... FOR UPDATE 是关键,但要注意锁范围
同一张流水号表、同一个 name 值,多个并发插入会排队等锁,保证流水号严格递增不重复。但如果业务需要多类流水号(如订单号、退款号、出库单号),必须为每类设独立 name,否则互相阻塞。
锁粒度取决于 WHERE 条件是否走主键或唯一索引。如果 name 是主键,SELECT ... FOR UPDATE 只锁对应行;如果没索引,会升级为表锁,彻底扼杀并发性能。
- 确认
seq_generator.name是PRIMARY KEY或至少有UNIQUE INDEX - 避免在触发器里对同一
name多次SELECT ... FOR UPDATE,MySQL 不允许重复加锁 - 超时控制靠
innodb_lock_wait_timeout(默认 50 秒),高并发下建议调低,让失败更快暴露
触发器无法回滚外部事务,错误处理要格外小心
触发器内抛异常(比如除零、空指针、更新失败)会导致整个 INSERT 回滚,这点没问题;但如果你在触发器里调用了外部服务、写日志文件或发消息,这些操作不会随事务回滚——业务上可能出现“流水号已发、但主表没插成”的脏状态。
更隐蔽的问题是:触发器里执行 UPDATE seq_generator 成功,但后续主表 INSERT 因唯一键冲突失败,这时流水号已跳过,无法回收。
- 流水号“跳号”是常态,业务系统得接受——只要不重复、可追溯,跳号不影响对账
- 绝对不要在触发器里做非事务性操作(如 HTTP 请求、文件写入、Redis 写入)
- 测试时一定要模拟并发插入(用脚本起 10+ 线程),观察是否出现重复号或锁等待超时
真正难的不是写触发器,而是设计好流水号表的分片策略、监控跳号率、以及和下游系统对齐“号段预分配”逻辑。这些不在触发器里解决,但漏掉任何一个,上线后都会出问题。


















