真正能落地的流水号方案必须分离号段分配与格式拼接,sql server 用 update ... output 原子取号,mysql 用事务+for update 或 insert ... on duplicate key update,日期分片防跨天重复,拼接纯字符串操作且由应用层完成。

不能靠 SELECT MAX() + 1,也不能依赖应用层拼接或单次 GETDATE() 调用——高并发下必然重复、跳号或死锁。真正能落地的方案,必须把「号段分配」和「格式拼接」彻底分离,且号段分配本身要原子、可回滚、带隔离保障。
SQL Server 必须用 UPDATE ... OUTPUT 原子取号
这是唯一绕过事务隔离级别依赖、保证强一致性的写法。单条 UPDATE 完成“读当前值→加1→写回→返回新值”四步,中间无间隙。
-
UPDATE必须作用在专用号段表(如SeqConfig),字段至少含Key(业务类型)、CurrentValue、LastUpdate -
WHERE Key = @BizType条件不可省,否则可能误更新多行;表必须有主键或唯一索引,否则锁会升级为表锁 - 必须带
OUTPUT INSERTED.CurrentValue,不能靠后续SELECT查——那会引入竞争窗口 - 别用
FORMAT()补零:性能差、线程不安全、文化设置可能导致空格;固定6位补零应写成RIGHT('000000' + CAST(@num AS VARCHAR(10)), 6)
MySQL 需显式事务 + FOR UPDATE 或 INSERT ... ON DUPLICATE KEY UPDATE
MySQL 没有 OUTPUT 子句,必须靠锁或冲突机制保原子性。自动提交模式下 FOR UPDATE 无效,START TRANSACTION 是硬性前提。
- 若用单行计数器(如
seq_generator表),必须先SELECT ... FOR UPDATE锁住该行,再UPDATE,最后COMMIT;漏掉START TRANSACTION就等于没锁 - 更推荐按日期分片:建表
seq_counter(seq_date DATE PRIMARY KEY, seq_value INT),用INSERT INTO ... ON DUPLICATE KEY UPDATE seq_value = LAST_INSERT_ID(seq_value + 1),再立刻SELECT LAST_INSERT_ID()取值 - 所有
WHERE条件必须命中主键或唯一索引,否则FOR UPDATE会锁全表,高并发下雪崩 - 日期统一用
CURDATE()或CONVERT(char(8), GETDATE(), 112),避免NOW()多次调用返回不同日期
日期重置逻辑最容易引发跨天冲突
按天重置(如 ORD202610150001)看似简单,但凌晨多个请求同时发现“当天无记录”,就会各自插入初始值,导致序号从1开始重复。
- 解决方案是把日期作为联合主键一部分:例如
Key = 'ORDER'和DatePart = '20261015'共同构成唯一约束 - 插入前先
INSERT SELECT尝试写入新日期行,用IF @@ROWCOUNT = 0判断是否已存在,再执行UPDATE - 别用
GETDATE()直接拼WHERE条件——不同会话执行时间毫秒级差异可能导致查不到刚插入的行
流水号拼接必须在号段分配之后完成
拼接是纯字符串操作,不是并发安全的关键点。数据库只负责“给号”,不负责解释业务语义。
- 前缀、中缀、后缀直接用
+拼接,不要在存储过程中做拼音缩写、组织路径展开等逻辑——那是应用层职责 - 如果业务要求总长固定(如14位),建议在应用层校验,数据库层不做截断——截断可能掩盖真实并发问题
- 避免在拼接逻辑里调用
REPLACE()或嵌套CONVERT(),每多一层都增加执行计划不稳定风险 -
SEQUENCE对象只适合弱一致性场景:它快、轻量,但NEXT VALUE FOR不受事务控制——事务回滚后号已跳过,且无法按业务类型隔离
真正难的从来不是“怎么拼”,而是“怎么取”——号段分配那一步稍有松动,整个流水号系统就失去可信基础。很多团队卡在“看起来能跑通”,却在大促或批量导入时突然爆出重复,问题几乎全出在取号环节的原子性缺失上。











