业务序列号不能直接用AUTO_INCREMENT或IDENTITY生成,因其仅支持纯数字递增,无法动态拼接前缀、年月及固定位数流水号;高并发下SELECT MAX()+1易导致丢号或重复,属竞态逻辑错误。

为什么不能直接用 AUTO_INCREMENT 或 IDENTITY 生成业务序列号
因为业务序列号往往带前缀、年月、流水位数(如 ORD2024050001),而 AUTO_INCREMENT 只提供纯数字递增,无法动态拼接格式;同时高并发下靠 SELECT MAX() + 1 会丢号或重复——这不是数据一致性问题,是竞态导致的逻辑错误。
- MySQL 5.7 及更早版本没有原生
SEQUENCE对象,得靠表+行锁模拟 - PostgreSQL / Oracle / SQL Server 2012+ 支持
SEQUENCE,但直接NEXTVAL拼字符串仍需在存储过程中做格式化,且要注意事务隔离级别 - 所有方案都必须保证「获取号段 → 格式化 → 写入业务表」原子性,否则中间出错会导致号空缺或重用
MySQL 中用单行锁表模拟序列号生成器
建一张只有一行的配置表,用 SELECT ... FOR UPDATE 加行锁,避免并发读写冲突:
CREATE TABLE seq_generator (
name VARCHAR(50) PRIMARY KEY,
next_val BIGINT NOT NULL
);
INSERT INTO seq_generator VALUES ('order_no', 1);
存储过程里这样用:
DELIMITER $$
CREATE PROCEDURE gen_order_no(OUT out_no VARCHAR(20))
BEGIN
DECLARE cur_val BIGINT;
START TRANSACTION;
SELECT next_val INTO cur_val FROM seq_generator WHERE name = 'order_no' FOR UPDATE;
UPDATE seq_generator SET next_val = cur_val + 1 WHERE name = 'order_no';
SET out_no = CONCAT('ORD', DATE_FORMAT(NOW(), '%Y%m'), LPAD(cur_val, 4, '0'));
COMMIT;
END$$
DELIMITER ;
- 必须显式
START TRANSACTION,否则FOR UPDATE在自动提交模式下无效 -
LPAD(cur_val, 4, '0')是关键:确保流水号固定 4 位,不是cur_val + 10000这类取巧写法(会溢出) - 别在事务外调用该过程后再插入业务表——必须把「取号 + 插入业务记录」包进同一事务,否则可能生成了号却写入失败,造成空号
PostgreSQL 使用 SEQUENCE 配合 to_char() 安全拼接
原生 SEQUENCE 性能好、无锁争用,但要注意:NEXTVAL 是事务无关的,一旦调用就不可回滚,所以必须在确定要生成号之后再取值:
CREATE SEQUENCE order_seq START 1 INCREMENT 1;
存储过程(PL/pgSQL)示例:
CREATE OR REPLACE FUNCTION gen_order_no()
RETURNS VARCHAR AS $$
DECLARE
seq_num BIGINT;
BEGIN
seq_num := NEXTVAL('order_seq');
RETURN 'ORD' || TO_CHAR(CURRENT_DATE, 'YYYYMM') || LPAD(seq_num::TEXT, 4, '0');
END;
$$ LANGUAGE plpgsql;
-
TO_CHAR(CURRENT_DATE, 'YYYYMM')比拼EXTRACT(YEAR FROM ...)更简洁,且时区安全(用CURRENT_DATE而非NOW()避免时分秒干扰) - 不要在触发器里调用这个函数后又做条件判断是否插入——
NEXTVAL已消耗,跳过插入也会留空号 - 如果需要按月重置流水(如每月从 0001 开始),就得用带条件的
SEQUENCE表(比如每条记录存 month + last_val),而不是单个全局序列
SQL Server 的 SEQUENCE 和 FORMAT() 组合陷阱
SQL Server 2012+ 支持 SEQUENCE,但 FORMAT() 函数在大数据量循环中性能差,且不支持所有语言环境。推荐用 RIGHT('0000' + CAST(@n AS VARCHAR), 4) 替代:
CREATE SEQUENCE order_seq AS BIGINT START WITH 1 INCREMENT BY 1;
存储过程中使用:
DECLARE @seq_num BIGINT = NEXT VALUE FOR order_seq;
DECLARE @no VARCHAR(20) = 'ORD'
+ FORMAT(GETDATE(), 'yyyyMM')
+ RIGHT('0000' + CAST(@seq_num AS VARCHAR(10)), 4);
-
FORMAT()在 SQL Server 2012–2014 中有已知性能问题,尤其在游标或循环内反复调用;2016+ 改进明显,但仍建议用字符串拼接兜底 - 注意
GETDATE()是事务开始时间戳,若过程执行跨月(比如 23:59:59 调用,实际写入在次日 00:00:01),月份部分会错——应提前计算并缓存@ym = FORMAT(GETDATE(), 'yyyyMM') - SQL Server 的
SEQUENCE不支持按范围预分配,高并发下仍可能因日志争用变慢,必要时考虑加CACHE 100提升吞吐
真正难的不是拼格式,而是决定「号段归属权」:是每次业务操作都实时申请一个号,还是批量预取一段号由应用层分配。后者减少数据库压力,但增加号管理复杂度和宕机丢失风险。选哪种,得看你的可用性要求和运维能力。

















