真正优雅的方案是用原子语句取号并让日期维度参与隔离:mysql用insert...on duplicate key update+last_insert_id(),sql server用update...output;表需建(biz_type,date_part)联合唯一索引;号段生成与业务写入必须同事务。

不能靠 SELECT MAX() + 1,也不能靠应用层拼接时间戳加随机数——高并发下必然重复或死锁。真正优雅的方案,是把“取号”压缩成单条原子语句,并让日期维度参与隔离。
MySQL 必须用 INSERT ... ON DUPLICATE KEY UPDATE + LAST_INSERT_ID()
这是唯一能兼顾原子性、可回滚、无显式锁、且不依赖事务启动时机的方式。MySQL 没有 OUTPUT 子句,SELECT ... FOR UPDATE 又要求事务已开启,而 INSERT ... ON DUPLICATE KEY UPDATE 在语句执行时自动触发唯一键冲突处理,天然带行级原子更新语义。
- 表结构必须含联合唯一索引:
(biz_type, date_part),不能只靠主键id -
INSERT INTO seq_table (biz_type, date_part, next_val) VALUES (@type, @date, 1) ON DUPLICATE KEY UPDATE next_val = next_val + 1—— 这条语句本身完成“查+增+写”三步 - 立刻跟
SELECT LAST_INSERT_ID() INTO @next_no,它返回的是本次INSERT实际影响的自增值(首次插入为1,冲突更新后为原值+1) - 严禁在同个事务里先
SELECT再UPDATE,窗口期哪怕只有几毫秒,也足够并发请求撞出重复
SQL Server 必须用 UPDATE ... OUTPUT INSERTED.CurrentValue
SQL Server 的 OUTPUT 是硬性优势,它让单条 UPDATE 同时完成读旧值、写新值、返新值四件事,全程不释放锁、不暴露中间态。比加 WITH (TABLOCKX) 或调高隔离级别更底层、更轻量。
-
UPDATE必须带精确WHERE Key = @BizType AND DatePart = @DatePart,且该组合必须是主键或唯一索引,否则可能锁整张表 - 只能用
OUTPUT INSERTED.CurrentValue拿结果,不能SELECT后再处理——那已经不是原子操作了 - 号段表字段至少含:
Key(业务类型)、DatePart(如'20260827')、CurrentValue、LastUpdate -
DatePart必须预计算好传入,比如CONVERT(char(8), GETDATE(), 112),别在WHERE里实时算,毫秒级时间差会导致查不到刚插入的当天记录
日期维度必须参与隔离,且格式化必须交给应用层
按天重置序号(如 ORD202608270001)时,“当天无记录”是并发冲突高发点。多个会话同时发现没数据,就会各自插入初始值,首号直接重复。格式化补零若放在存储过程里,不仅性能差(FORMAT() 线程不安全),还污染了发号逻辑的单一职责。
-
DatePart字段统一用确定性表达式:SQL Server 用CONVERT(char(8), GETDATE(), 112),MySQL 用DATE_FORMAT(NOW(), '%Y%m%d'),避免区域设置或毫秒偏差干扰 - 插入新日期前,必须用
IF @@ROWCOUNT = 0(SQL Server)或ROW_COUNT() = 0(MySQL)判断是否已存在,再决定走INSERT还是UPDATE - 纯数字序号(如
1、42)由存储过程返回,前缀拼接和补零(如RIGHT('0000' + CAST(@n AS VARCHAR(4)), 4))一律交给应用层做
最容易被忽略的一点:生成号段和写入业务表必须在同一事务内完成。如果发号成功但业务插入失败,又没回滚号段表,就会导致序号“丢失”——这不是 bug,是设计缺陷。所有涉及状态变更的操作,必须共用一个事务边界。










