基于SQL Server的Java应用每日重置序列及竞态规避方案咨询
每日自动重置序列实现与竞态规避实践
核心实现路径
结合Docker部署Java多副本、配套SQL Server的现有架构,可行的实现路径主要分为两类:
- 数据库层懒加载重置方案:以SQL Server作为序列的唯一可信存储,不依赖定时任务,在请求第一次访问当日序列时自动完成跨天重置,从0开始逐次递增,适配现有技术栈不需要额外引入中间件。
- 应用层集群协同方案:通过集群分布式锁协调各Java副本的序列生成逻辑,结合本地缓存减少数据库访问频率,适合QPS极高的访问场景,但整体实现复杂度更高。
竞态条件规避的核心最佳实践
所有设计要围绕“单一事实源、原子操作、避免三段式读写”的原则,具体落地要点:
- 序列的唯一可信源必须是SQL Server,禁止每个Java副本在本地独立维护序列值,否则多副本并行运行时必然出现重复值。
- 设计专门的序列映射表,核心字段包括:
seq_date:date类型,存储当前序列对应的自然日current_value:bigint类型,存储当前已递增到的最新值version:int类型,用于实现乐观锁,避免并发更新冲突
- 取值逻辑必须全部包裹在可串行化事务中,禁止采用“应用层先查当前值→计算新值→回写新值”的三段式逻辑,这种写法在SQL Server默认的读已提交隔离级别下,极易出现多节点同时读到相同旧值、回写相同新值的竞态问题。正确的做法是把值的递增操作下推到单条原子UPDATE语句完成,示例逻辑参考:
BEGIN TRANSACTION; DECLARE @today DATE = CAST(GETDATE() AS DATE); -- 初始化当日序列(首次访问时触发,初始值设为-1保证第一次取值为0) IF NOT EXISTS (SELECT 1 FROM DailySeq WHERE seq_date = @today) BEGIN INSERT INTO DailySeq(seq_date, current_value, version) VALUES (@today, -1, 1); END -- 乐观锁更新,同时完成值递增 UPDATE DailySeq SET current_value = current_value + 1, version = version + 1 WHERE seq_date = @today AND version = @传入的当前版本号; -- 如果更新影响行数为0,说明存在并发冲突,触发退避重试 SELECT current_value FROM DailySeq WHERE seq_date = @today; COMMIT TRANSACTION;
- 跨天的重置逻辑采用懒加载触发,别依赖零点定时作业执行重置:定时作业存在天然时间差,很容易出现0点整的请求还未完成写入,就被提前重置序列值的问题,最终导致当日序列出现跳值、甚至重复值。只有当事务中检测到当前日期和表内存储的
seq_date不匹配时,才在同一个事务内完成值重置,彻底覆盖跨天边界的所有请求。 - 并发冲突的重试逻辑要加随机退避间隔:当乐观锁更新失败时,不要无限制瞬时重试,一般设置520ms的随机退避,最多重试35次即可,避免占满数据库连接池。
- 跨天边界的请求匹配要以请求发生的实际自然日为准,不要以服务器处理请求的时间为准,避免网络延迟导致的请求拿到错配日期的序列值。
方案选型参考
- 纯数据库实现方案:单库可支撑每秒数千到上万次的序列取值请求,序列重复率为0,运维成本最低,是绝大多数业务场景的首选。
- 批量预取优化方案:如果序列取值QPS达到万级以上,可以在数据库实现的基础上,让每个Java副本一次从数据库预取一段连续值到本地内存,用完后再申请下一段,减少数据库交互频率。这种方案需要必须在副本感知到跨天时立刻清空本地缓存的未使用序列值,避免出现跨天的序列值断层。
- 不推荐依赖SQL Server原生SEQUENCE对象加定时作业重置的方案:原生SEQUENCE本身不支持按日期维度自动重置,要实现跨天重置就得靠定时作业在零点把值改回起点,这种边界上的竞态问题很难彻底解决,后续维护成本很高。
内容的提问来源于stack exchange,提问作者TMac
相关产品推荐
相关产品推荐

