You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

无重复Java每日唯一序列号生成技术方案问询

可行实现方案

核心思路

基于时间维度+节点标识+原子计数器的组合,完全依赖内存计算,无需数据库或文件存储,同时保证单日唯一性(包括JVM重启场景),完美匹配所有要求:

  1. 序列号结构:8位数字,格式为 [节点ID][当日秒数编码(5位)][秒内计数器(2位)]

    • 第1位:节点ID(0-9,最多支持10个节点实例,符合“首位预留标识节点”要求)
    • 第2-6位:当日00:00:00到当前时间的秒数,补前导0至5位(范围:00000 ~ 86399,覆盖一天所有秒数)
    • 第7-8位:秒内计数器(00 ~ 99,每秒最多生成100个序列号,满足“每秒最小99个”的能力要求)
  2. 关键特性实现:

    • JVM重启不重复:利用时间单向递增的特性,重启后的当前时间必然晚于重启前,因此秒内计数器的初始值基于当前毫秒数计算(currentMillis / 10),保证重启后计数器起始值不低于重启前的最大值,从根源避免重复。
    • 无外部存储依赖:所有状态(当前秒数、计数器)均存储在内存中,无需数据库或文件持久化。
    • 跨天重复:每日秒数从00000重新开始,自然实现跨天序列号重复。

具体实现细节

  • 节点ID配置:通过环境变量、系统属性或配置文件注入每个节点唯一的ID(0-9),确保多节点间首位不重复。
  • 时间计算:使用Instant类获取当前时间,计算当日0点的时间戳,进而得到当日已过秒数。
  • 原子计数器:使用AtomicLong保证多线程下的线程安全,每次生成序列号时:
    1. 获取当前秒数,与上次记录的秒数对比;
    2. 若秒数不同,重置计数器为当前秒数*100 + (当前毫秒数/10);
    3. 若秒数相同,计数器自增1;
    4. 截取计数器的最后7位(当日秒数5位+计数器2位),拼接节点ID得到8位序列号。

Java代码示例

import java.time.Instant;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.concurrent.atomic.AtomicLong;

public class DailySerialNumberGenerator {
    // 节点ID(0-9,需每个节点配置唯一值)
    private final int nodeId;
    // 原子计数器,存储格式:当日秒数*100 + 秒内计数器
    private final AtomicLong counter = new AtomicLong(0);
    // 上次生成序列号的秒数
    private volatile long lastSecond = -1;

    public DailySerialNumberGenerator(int nodeId) {
        if (nodeId < 0 || nodeId > 9) {
            throw new IllegalArgumentException("Node ID must be between 0 and 9");
        }
        this.nodeId = nodeId;
    }

    public String generate() {
        Instant now = Instant.now();
        long currentSecond = getCurrentDaySecond(now);
        long currentMillis = now.toEpochMilli() % 1000;

        // 重置计数器:进入新的秒,或JVM重启后首次生成
        if (currentSecond != lastSecond) {
            synchronized (this) {
                if (currentSecond != lastSecond) {
                    // 初始值 = 当前秒数*100 + 毫秒/10(保证重启后起始值不重复)
                    long initialValue = currentSecond * 100 + (currentMillis / 10);
                    counter.set(initialValue);
                    lastSecond = currentSecond;
                }
            }
        }

        // 自增计数器
        long currentCounter = counter.incrementAndGet();
        // 截取最后7位(当日秒数5位+计数器2位),补前导0
        String suffix = String.format("%07d", currentCounter % 10000000);
        // 拼接节点ID,得到8位序列号
        return nodeId + suffix;
    }

    // 计算当日00:00:00到当前时间的秒数
    private long getCurrentDaySecond(Instant now) {
        LocalDate today = LocalDate.ofInstant(now, ZoneId.systemDefault());
        Instant dayStart = today.atStartOfDay(ZoneId.systemDefault()).toInstant();
        return (now.toEpochMilli() - dayStart.toEpochMilli()) / 1000;
    }

    // 测试示例
    public static void main(String[] args) {
        DailySerialNumberGenerator generator = new DailySerialNumberGenerator(1);
        for (int i = 0; i < 100; i++) {
            System.out.println(generator.generate());
        }
    }
}

方案验证

  1. JVM重启不重复:重启后,当前时间晚于重启前,currentMillis更大,计数器初始值currentSecond*100 + (currentMillis/10)必然大于重启前的计数器最大值,因此不会重复。
  2. 无外部存储:所有状态均在内存中,无需数据库或文件。
  3. 跨天重复:每日0点后,currentSecond从0重新开始,序列号后缀从0000000开始,自然跨天重复。
  4. 每秒生成能力:每秒最多生成100个序列号,满足“最小99个”的要求。
  5. 8位数字格式:节点ID(1位)+7位后缀,共8位数字。
  6. 多节点唯一:每个节点配置不同的ID,首位不同,避免多节点间序列号重复。

内容的提问来源于stack exchange,提问作者Mohamed Saligh

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.09 19:22:28