关注我的公众号:【编程朝花夕拾】,可获取首发内容。

01 引言
你可曾遇到订单编号的生成、分布式ID的生成等业务业务功能。成熟的轮子如:UUID、雪花算法、美团Leaf、百度UidGenerator、滴滴Tinyid等。但是如果想要加入自己项目的业务参数,可能就没有那么通用了。
我们就需要设计属于自己唯一编码的生成规则。我们将分别从单机到高并发环境下拆解不同的思路。
02 设计思路
唯一编码设计的核心是平衡唯一性、有序性、性能、可用性与存储效率。主要的分为三种:
- 中心化强一致:借助数据库/Redis原子发号,保证严格递增,并通过号段预取缓解性能瓶颈。
- 去中心化组合式(如雪花算法):节点用"时间戳+机器ID+序列号"本地拼装,高性能且趋势递增,但依赖时钟。
- 概率型 (如
UUID):随机生成,无协调开销,但无序且存储较大。
设计本质是在"无协调本地生成"与"有协调中心发号"间权衡,用位分配或号段预取打破性能与唯一性的僵局。
03 单机设计方案
大多数公司的架构都是分布式架构,单机设计方案可能用的比较少,但是单机设计方案是基础。其他复杂的设计方案都是在其基础上演变而来。
3.1 设计思想
不依赖任何外部中间件,完全在 JVM 进程内完成发号。通过时间戳位移保证宏观有序,通过PID 与随机数混合区分进程,通过 AtomicLong 保证线程安全。
3.2 代码案例
java
public final class UniqueIdUtils {
private static final AtomicLong SEQ = new AtomicLong(0);
// 时间戳左移 20 位,腾出低位给自增与随机部分
private static final long TIME_SHIFT = 20L;
// 进程指纹:PID ^ 20 位随机数
private static final long PROCESS_SEED;
static {
// 获取进程(如:53032@shsx-dell-0001)
String name = ManagementFactory.getRuntimeMXBean().getName();
// 获取PID
long pid = name.contains("@") ? Long.parseLong(name.split("@")[0]) : 0L;
// 2^20 = 1,048,576, 获取[0, 1,048,576)随机数
long rnd = ThreadLocalRandom.current().nextLong(1L << 20);
// 混合特征保留低20位
PROCESS_SEED = (pid ^ rnd) & ((1L << 20) - 1);
}
private UniqueIdUtils() {}
/**
* 生成唯一序列号(Long 型)
*/
public static long nextId() {
long timePart = System.currentTimeMillis() << TIME_SHIFT;
long seq = SEQ.incrementAndGet() & ((1L << 20) - 1);
return timePart | PROCESS_SEED | seq;
}
/**
* 生成唯一序列号(String 型,便于日志与接口传输)
*/
public static String nextIdStr() {
return Long.toUnsignedString(nextId());
}
public static void main(String[] args) {
System.out.println(nextIdStr());
System.out.println(nextIdStr());
}
}
当然还有更简单的,世界使用毫秒值再加上随机数。
3.3 优缺点
- 优点:自然毋庸置疑,零外部依赖;代码极简;纯内存操作性能极高;宏观时间有序。
- 缺点:多机同
PID场景下理论上存在冲突;重启后无状态,无法追溯历史;分布式系统自然无法使用
04 分布式高并发方案
4.1 设计思想
采用中心化强一致的设计思路,利用 Redis 的 单线程执行模型 ,通过 INCR / INCRBY 实现全局原子递增。为降低 Redis 访问频率,引入号段模式:每次从 Redis 批量申请一段号(如 1000 个),缓存在本地内存中,后续直接在本地原子分配。
4.2 代码案例
java
public class RedisSegmentSequence {
private final StringRedisTemplate redisTemplate;
private final String redisKey;
private final long step;
// 本地号段缓存
private volatile long localMax = -1;
private final AtomicLong localSeq = new AtomicLong(-1);
public RedisSegmentSequence(StringRedisTemplate redisTemplate,
String redisKey,
long step) {
this.redisTemplate = redisTemplate;
this.redisKey = redisKey;
this.step = step;
}
/**
* 获取下一个唯一 ID
*/
public synchronized long nextId() {
long current = localSeq.incrementAndGet();
if (current > localMax) {
// 本地号段耗尽,重新向 Redis 申请
fetchNextSegment();
current = localSeq.incrementAndGet();
}
return current;
}
private void fetchNextSegment() {
// Redis 原子自增 step,返回的是新的最大值
Long max = redisTemplate.opsForValue().increment(redisKey, step);
if (max == null) {
throw new IllegalStateException("Redis increment failed");
}
this.localMax = max;
// 本地从 max - step 开始分配
this.localSeq.set(max - step);
}
}
Spring Bean 配置示例:
java
@Configuration
public class SequenceConfig {
@Bean
public RedisSegmentSequence orderSequence(StringRedisTemplate redisTemplate) {
// step = 1000:每次向 Redis 申请 1000 个号,本地无 Redis 调用
return new RedisSegmentSequence(redisTemplate, "seq:order", 1000);
}
}
4.3 优缺点
- 优点:性能极高(本地原子操作,无网络
RTT),减少了直接与Redis的交互。 - 缺点:宕机/重启时本地未用完的号段会丢失,造成
Redis号段"跳号"
4.4 优化
号段模式虽然可能存在跳号,但对与业务没有影响。非常适用于高并发场景。并发量少又嫌浪费号段的话,我们可以减少号段的范围,或者直接使用INCR逐条获取,可以有效的防止号段的浪费,但是必然增加于Redis的交互。
来看看我们公司业务设计方案:通过业务码+yyyyMMddHHmmss+3位自增序列
如:UG20260521153035001
markdown
UG + 20260521153035 + 001
│ │ │
│ │ └── 3位自增序列 (001-999)
│ └── 14位时间戳 (秒级精度)
└── 2位业务码
UG:业务码,根据业务模块定义20260521153035:时间点,2026-05-21 15:30:35001:自增序列号,每秒最多999个号,这个根据自己业务的并发设置。
思路:主要通过Redis控制同一秒内的自增序列即可,1s过后之前的序列自动过期。
java
Component
public class SerialNumberGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
// Lua脚本:自增+设置过期时间,原子操作
private static final String LUA_SCRIPT =
"local current = redis.call('incr', KEYS[1]) " +
"if current == 1 then " +
" redis.call('expire', KEYS[1], ARGV[1]) " +
"end " +
"return current";
private RedisScript<Long> redisScript = new DefaultRedisScript<>(LUA_SCRIPT, Long.class);
public String generate(String bizCode) {
LocalDateTime now = LocalDateTime.now();
String timeStr = now.format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));
String redisKey = String.format("seq:%s:%s", bizCode, timeStr);
// 原子执行:自增+设置过期时间
Long sequence = redisTemplate.execute(
redisScript,
Collections.singletonList(redisKey),
"2" // 保险起见,可以设置2秒过期
);
if (sequence == null || sequence > 999) {
throw new RuntimeException("序列号生成失败或超出上限");
}
return bizCode + timeStr + String.format("%03d", sequence);
}
}
05 小结
唯一编码的的场景有很多,如验证码、订单号、分布式ID等,需要唯一编码时,如果需要自己设计,设计的思路基本一致。
老铁们,你们都是怎么设计的?