SQL Server高并发生成唯一订单号
在高并发业务场景下,订单号的生成需要同时满足全局唯一性、趋势递增性和高性能。如果使用随机字符串或UUID,虽然能保证唯一性,但会破坏数据库索引的聚簇结构,导致页分裂和写入性能下降。本文将聚焦于使用SQL Server内置机制,实现高并发下稳定且高效生成唯一订单号的完整方案。
常见订单号生成方案对比
在深入SQL Server实现之前,我们先简要对比几种主流方案:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| UUID / GUID | 全局唯一,无需协调 | 无序,索引碎片严重,长度过长 | 分布式系统,对顺序无要求 |
| 雪花算法 | 趋势递增,高性能 | 依赖机器时钟,需要工作节点ID管理 | 分布式高并发应用 |
| 数据库自增ID | 实现简单,严格递增 | 单点瓶颈,步长固定,扩展性差 | 低并发单库应用 |
| SQL Server SEQUENCE | 高性能,无事务阻塞,支持设置步长 | 需要数据库对象管理,跨库需额外处理 | 高并发单体或读写分离架构 |
| Redis INCR | 高性能,原子操作 | 引入额外组件,需考虑持久化和宕机恢复 | 已使用Redis的架构 |
如果技术栈以SQL Server为核心且希望减少外部依赖,使用SEQUENCE对象配合SEQUENCE的缓存机制是最佳实践之一。
基于SQL Server SEQUENCE的高并发实现
SQL Server从2012版本开始引入了SEQUENCE对象,它独立于表存在,可以按指定步长和缓存大小生成连续或跳跃的数值。在高并发环境下,SEQUENCE不会阻塞事务,性能远优于表自增列。
创建SEQUENCE并设计订单号格式
假设我们需要18位长度的订单号,格式为:YYYYMMDD + 8位序列号。序列号部分使用SEQUENCE生成。
-- 创建序列:从1开始,步长为1,缓存1000个值(提升并发性能) CREATE SEQUENCE OrderSeq START WITH 1 INCREMENT BY 1 CACHE 1000;
获取下一个序列值并使用格式化补零:
-- 生成当日订单号:日期 + 8位序列号(不足左补零)
SELECT
CONVERT(VARCHAR(8), GETDATE(), 112)
+ RIGHT('00000000' + CAST(NEXT VALUE FOR OrderSeq AS VARCHAR(8)), 8)
AS OrderNo;处理每日重置场景
许多业务要求订单号每天从0开始重新计数。针对这个需求,可以创建一个调度任务(SQL Agent Job或在应用中处理),每日零点重置序列:
-- 重置序列(每日执行) ALTER SEQUENCE OrderSeq RESTART WITH 1;
更健壮的方式是使用包含日期的存储过程,内部判断是否跨日并自动重置:
CREATE PROCEDURE usp_GenerateOrderNo
@OrderNo VARCHAR(20) OUTPUT
AS
BEGIN
SET NOCOUNT ON;
DECLARE @Today VARCHAR(8) = CONVERT(VARCHAR(8), GETDATE(), 112);
DECLARE @LastDate VARCHAR(8);
DECLARE @NextSeq INT;
-- 使用一个辅助表记录上次生成日期
-- 这里简化处理,直接结合SEQUENCE进行判断
-- 方法:先取SEQUENCE值,再判断是否需要重置
SELECT @NextSeq = NEXT VALUE FOR OrderSeq;
-- 如果序列值达到了预设的每日上限(例如100万),则自动重置
-- 或者通过额外的日期标记表来判断跨日
-- 示例:跨日重置逻辑在应用层或作业中处理
SET @OrderNo = @Today + RIGHT('00000000' + CAST(@NextSeq AS VARCHAR(8)), 8);
END;使用表存储当前序号(无SEQUENCE的兼容方案)
如果数据库版本是2008及更早版本,或者由于权限原因无法使用SEQUENCE,可以使用一张独立的序号表配合UPDATE ... OUTPUT实现高并发取号:
-- 创建序号表
CREATE TABLE SequenceTable (
SeqName NVARCHAR(50) PRIMARY KEY,
CurrentValue INT NOT NULL
);
-- 插入订单号序列初始值
INSERT INTO SequenceTable (SeqName, CurrentValue) VALUES ('OrderSeq', 0);-- 高并发下安全获取下一个序号(使用UPDLOCK + ROWLOCK + 事务)
DECLARE @NextSeq INT;
UPDATE SequenceTable WITH (UPDLOCK, ROWLOCK)
SET @NextSeq = CurrentValue + 1,
CurrentValue = CurrentValue + 1
WHERE SeqName = 'OrderSeq';
-- 组合成订单号
SELECT
CONVERT(VARCHAR(8), GETDATE(), 112)
+ RIGHT('00000000' + CAST(@NextSeq AS VARCHAR(8)), 8)
AS OrderNo;该方案通过UPDLOCK和ROWLOCK锁提示,将并发压力降到行级别,性能远高于使用MAX(OrderID)+1的方式。
使用OUTPUT子句实现在插入时直接返回订单号
对于需要在订单表写入时直接获取生成后的订单号的场景,可以结合OUTPUT子句完成:
CREATE TABLE Orders (
OrderNo VARCHAR(20) PRIMARY KEY,
CustomerID INT,
OrderDate DATETIME DEFAULT GETDATE()
);
-- 插入并返回生成的订单号
DECLARE @OrderNo VARCHAR(20);
SET @OrderNo = CONVERT(VARCHAR(8), GETDATE(), 112)
+ RIGHT('00000000' + CAST(NEXT VALUE FOR OrderSeq AS VARCHAR(8)), 8);
INSERT INTO Orders (OrderNo, CustomerID)
OUTPUT INSERTED.OrderNo
VALUES (@OrderNo, 12345);这种方式保证了订单号在事务内生成并立即写入,避免了重复取号但未使用导致的号段浪费(当然,SEQUENCE本身可能由于缓存丢失产生跳跃,但不会产生重复)。
高并发下的性能优化要点
SEQUENCE缓存设置
缓存大小直接影响并发性能。缓存1000意味着SQL Server每次从磁盘读取1000个序列值到内存,应用层并发取号时完全不涉及磁盘IO,直到缓存耗尽才再次读取。对于需要严格控制号段连续性的场景(如银行流水),可以将缓存设为NO CACHE,但并发性能会下降。
-- 无缓存模式,保证号段严格连续(性能较低) CREATE SEQUENCE OrderSeqStrict START WITH 1 INCREMENT BY 1 NO CACHE;
避免不必要的排序
订单号作为主键或聚集索引键时,由于日期在前、序列在后,写入基本是按时间顺序追加,索引维护开销很小。这与UUID完全随机写入形成鲜明对比。
如果业务需要跨日查询,可以在订单号上建立非聚集索引,或者直接使用OrderDate列作为索引,不要将日期冗余在订单号中。订单号中的日期主要是为了保证业务可读性和每日重置能力,而非查询性能。
批量生成优化
如果应用需要批量生成订单号(比如导入订单),可以使用sys.sp_sequence_get_range存储过程一次获取一个区间:
DECLARE @FirstSeqNum SQL_VARIANT, @LastSeqNum SQL_VARIANT; EXEC sys.sp_sequence_get_range @sequence_name = N'OrderSeq', @range_size = 1000, @range_first_value = @FirstSeqNum OUTPUT, @range_last_value = @LastSeqNum OUTPUT; SELECT CAST(@FirstSeqNum AS INT) AS FirstValue, CAST(@LastSeqNum AS INT) AS LastValue;
应用层拿到起始值和结束值后,可以在内存中循环生成1000个订单号,批量插入数据库,极大降低数据库交互次数。
典型问题与解决方案
问题1:跨日重置时并发冲突
每日零点重置序列可能会与正在取号的请求冲突。解决方案是使用TRY...CATCH捕获重置时的错误,或者将重置操作延迟到凌晨低峰期执行,并结合应用层重试逻辑。
问题2:序列缓存丢失导致跳跃
数据库重启会导致CACHE中未使用的序列值丢失,订单号会出现跳跃。如果业务不允许跳跃,必须使用NO CACHE,或者采用表加锁的方案。
问题3:高并发下死锁
使用表加锁方案时,如果锁粒度控制不当或事务过长,可能引发死锁。建议保持事务简短,仅包含获取序号这一个操作,不要将其他业务逻辑混入同一事务。
总结
使用SQL Server的SEQUENCE对象生成唯一订单号,在兼顾唯一性、趋势递增性和高并发性能方面表现优秀。配合日期前缀,还能满足业务可读性和每日重置的需求。对于无法使用SEQUENCE的旧版本,利用UPDLOCK和ROWLOCK锁定序号行是可靠的替代方案。
实际项目中还需要根据业务容忍度来权衡序列缓存大小、是否允许跳跃以及跨日重置策略。结合批量生成和OUTPUT子句,可以构建出每秒处理数万订单而稳定不冲突的订单号生成系统。