真正能落地的流水号方案必须分离号段分配与格式拼接,sql server用update+output原子取号,mysql用事务+for update或insert...on duplicate key update,日期分片需联合主键防跨天重复,应用层须将取号与业务操作置于同一事务。

直接用 SELECT MAX() + 1 再 UPDATE 生成流水号,在并发稍高时必然出错——不是重复,就是死锁或空号。真正能落地的方案,必须把「号段分配」和「格式拼接」彻底分开,且号段分配本身要原子、可回滚、带隔离保障。
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()多次调用返回不同日期
日期重置逻辑最容易引发跨天冲突
按天重置(如 ORD202609290001)看似简单,但凌晨多个请求同时发现“当天无记录”,就会各自插入初始值,导致序号从1开始重复。
- 解决方案是把日期作为联合主键一部分:例如
Key = 'ORDER'和DatePart = '20260929'共同构成唯一约束 - 插入前先
INSERT ... SELECT尝试写入新日期行,用IF @@ROWCOUNT = 0判断是否已存在,再执行UPDATE - 别用
GETDATE()直接拼WHERE条件——毫秒级差异可能导致刚插入的行查不到;应统一转成CONVERT(char(8), GETDATE(), 112) - 应用层必须把「取号」和「写业务数据」包进同一个事务;分开调用会导致号已生成但业务失败,造成空号或重试重号
真正难的不是拼字符串,而是让每次取号都像取一枚已编号的子弹——装填、击发、弹出,全程不可中断、不可复制、不可回退。任何试图在存储过程中混入拼音转换、动态前缀计算、多层 REPLACE() 的做法,都在给执行计划埋雷。











