最稳妥方式是在BEFORE INSERT触发器中通过原子递增计数表或序列生成编号:先校验business_type白名单,再用UPDATE...LAST_INSERT_ID()或NEXTVAL()获取新序号,最后拼接前缀与6位补零的数字部分,确保唯一性和并发安全。

触发器里怎么拼出带业务前缀的单据号
直接在 INSERT 前用触发器生成编号最稳妥,避免应用层并发冲突。核心是:取当前最大编号 → 解析数字部分 → +1 → 拼回前缀和补零。别用 SELECT MAX() 加锁查,容易死锁;改用带 FOR UPDATE SKIP LOCKED(PostgreSQL)或事务内加行锁(MySQL InnoDB)更安全。
常见错误是忽略业务类型字段为空或非法值,导致前缀拼错或报错。务必在触发器开头加判断:
- 检查
business_type是否在预设白名单里(如'SO'、'PO'、'INV') - 若为空或不合法,直接
RAISE EXCEPTION或设默认前缀并记录告警 - 数字部分建议统一用 6 位补零(
LPAD('123', 6, '0')),避免后期排序错乱
MySQL 中触发器读不到新插入的字段值?
MySQL 的 BEFORE INSERT 触发器里,NEW 是可写的,但必须显式赋值才能生效。比如你不能靠 SELECT ... INTO @next_no 再去改 NEW.bill_no,得直接写:
SET NEW.bill_no = CONCAT('SO-', LPAD(IFNULL((SELECT CAST(SUBSTRING_INDEX(MAX(bill_no), '-', -1) AS UNSIGNED) + 1 FROM orders WHERE bill_no LIKE 'SO-%' ORDER BY id DESC LIMIT 1), 1), 6, '0'));
但这样写有隐患:子查询不能用 MAX() 直接跨事务取最新值,高并发下会重复。更可靠的是建一张 seq_counter 表,每种业务类型占一行,用 UPDATE ... SET counter = LAST_INSERT_ID(counter + 1) 原子递增,再读 LAST_INSERT_ID() 拼号。
PostgreSQL 怎么避免序列号跳号或重复
PG 里推荐用 SEQUENCE + BEFORE INSERT 触发器组合。先为每类业务建独立序列:
CREATE SEQUENCE seq_so START 1 INCREMENT 1;
触发器里按 NEW.business_type 分支调用对应序列:
CASE NEW.business_type
WHEN 'SO' THEN NEW.bill_no := 'SO-' || LPAD(NEXTVAL('seq_so')::TEXT, 6, '0');
WHEN 'PO' THEN NEW.bill_no := 'PO-' || LPAD(NEXTVAL('seq_po')::TEXT, 6, '0');
ELSE RAISE EXCEPTION 'Unknown business_type: %', NEW.business_type;
END CASE;
注意:序列本身不回滚,但触发器执行失败时,已取的序列值作废——这是设计使然,不能强求绝对连续。真正要防重,得靠 bill_no 字段加唯一约束,让数据库兜底。
为什么不用应用层生成而坚持用触发器
应用层生成编号看似简单,但实际踩坑更多:分布式部署时没法共享状态、网络超时导致重试重复提交、事务未提交前其他请求查不到最新值。触发器把逻辑锁死在数据库侧,只要事务隔离级别是 READ COMMITTED 或更高,就能保证编号唯一且有序。
不过得留神两点:一是触发器逻辑别太重,避免拖慢写入;二是如果业务要求“编号可预测”(比如用户下单瞬间就要显示单号),那得提前在事务开始时就生成并缓存,不能等到 INSERT 才算——这点常被忽略,结果前端卡住等 DB 返回,体验变差。










