无重复Java每日唯一序列号生成技术方案问询
可行实现方案
核心思路
基于时间维度+节点标识+原子计数器的组合,完全依赖内存计算,无需数据库或文件存储,同时保证单日唯一性(包括JVM重启场景),完美匹配所有要求:
序列号结构:8位数字,格式为
[节点ID][当日秒数编码(5位)][秒内计数器(2位)]- 第1位:节点ID(0-9,最多支持10个节点实例,符合“首位预留标识节点”要求)
- 第2-6位:当日00:00:00到当前时间的秒数,补前导0至5位(范围:00000 ~ 86399,覆盖一天所有秒数)
- 第7-8位:秒内计数器(00 ~ 99,每秒最多生成100个序列号,满足“每秒最小99个”的能力要求)
关键特性实现:
- JVM重启不重复:利用时间单向递增的特性,重启后的当前时间必然晚于重启前,因此秒内计数器的初始值基于当前毫秒数计算(
currentMillis / 10),保证重启后计数器起始值不低于重启前的最大值,从根源避免重复。 - 无外部存储依赖:所有状态(当前秒数、计数器)均存储在内存中,无需数据库或文件持久化。
- 跨天重复:每日秒数从00000重新开始,自然实现跨天序列号重复。
- JVM重启不重复:利用时间单向递增的特性,重启后的当前时间必然晚于重启前,因此秒内计数器的初始值基于当前毫秒数计算(
具体实现细节
- 节点ID配置:通过环境变量、系统属性或配置文件注入每个节点唯一的ID(0-9),确保多节点间首位不重复。
- 时间计算:使用
Instant类获取当前时间,计算当日0点的时间戳,进而得到当日已过秒数。 - 原子计数器:使用
AtomicLong保证多线程下的线程安全,每次生成序列号时:- 获取当前秒数,与上次记录的秒数对比;
- 若秒数不同,重置计数器为
当前秒数*100 + (当前毫秒数/10); - 若秒数相同,计数器自增1;
- 截取计数器的最后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()); } } }
方案验证
- JVM重启不重复:重启后,当前时间晚于重启前,
currentMillis更大,计数器初始值currentSecond*100 + (currentMillis/10)必然大于重启前的计数器最大值,因此不会重复。 - 无外部存储:所有状态均在内存中,无需数据库或文件。
- 跨天重复:每日0点后,
currentSecond从0重新开始,序列号后缀从0000000开始,自然跨天重复。 - 每秒生成能力:每秒最多生成100个序列号,满足“最小99个”的要求。
- 8位数字格式:节点ID(1位)+7位后缀,共8位数字。
- 多节点唯一:每个节点配置不同的ID,首位不同,避免多节点间序列号重复。
内容的提问来源于stack exchange,提问作者Mohamed Saligh
相关产品推荐
相关产品推荐

