ID生成器
你好呀,我的老朋友!我是老寇,欢迎来到老寇IoT云平台!
模块路径:
laokou-common/laokou-common-id-generator支持两种策略,均实现
IdGenerator接口,可在 gRPC 请求参数中动态选择。
# 目录
# 1. 统一接口说明
所有生成器均实现 IdGenerator 接口:
public interface IdGenerator {
void init() throws Exception; // 初始化生成器
void close() throws Exception; // 关闭生成器(释放资源)
long nextId(BizType bizType); // 生成单个 ID
List<Long> nextIds(BizType bizType, int num); // 批量生成 ID
Instant getInstant(long snowflakeId); // 从雪花ID反推生成时间
long getDatacenterId(long snowflakeId); // 从雪花ID提取数据中心ID
long getWorkerId(long snowflakeId); // 从雪花ID提取机器ID
long getSequence(long snowflakeId); // 从雪花ID提取序列号
}
BizType为业务类型枚举,用于区分不同业务线的 ID 空间(如BizType.AUTH)。
# 2. Redis 分段 ID 生成器
# 2.1 原理说明
基于美团 Leaf-Segment 思路,采用双 Buffer 异步预加载方案:
┌─────────────────────────────────────────────────────────────────┐
│ Redis INCRBY (Lua 原子脚本) │
│ 每次批量申请号段(step=10000),返回本次号段的 maxId │
└──────────────────────┬──────────────────────────────────────────┘
│ 号段 [minId, maxId]
┌────────▼────────┐
│ SegmentBuffer │ 双 Buffer(A / B 交替使用)
│ ┌───────────┐ │
│ │ Segment A │◄─┼── 当前正在使用(AtomicLong 本地自增)
│ └───────────┘ │
│ ┌───────────┐ │
│ │ Segment B │◄─┼── 异步预加载(消耗到 loadFactor 时触发)
│ └───────────┘ │
└─────────────────┘
核心流程:
- 启动时通过 Lua 脚本
INCRBY key step原子申请首个号段 - 本地
AtomicLong.getAndIncrement()高速分配 ID,无需每次访问 Redis - 当前号段消耗到
loadFactor(默认 80%)时,虚拟线程异步加载下一号段到备用 Buffer - 当前号段耗尽,原子切换到备用 Buffer 继续分配,无中断
- 关闭时回写 cursor 到 Redis,重启后从上次位置继续,避免 ID 大幅跳跃
# 2.2 Lua 脚本(segment_alloc.lua)
-- KEYS[1] = 号段 Key
-- ARGV[1] = 步长(step)
local key = KEYS[1]
local step = tonumber(ARGV[1])
return redis.call('INCRBY', key, step)
-- 返回值 = 本次号段的 maxId
-- minId = maxId - step + 1
# 2.3 配置说明(application.yml)
spring:
id-generator:
segment:
node-id: 1 # 节点标识(可选)
configs:
AUTH: # BizType 枚举的 code 值,可配置多个
step: 10000 # 每次从 Redis 申请的号段步长(默认 10000)
key: "id-generator:segment:auth" # Redis 号段计数 Key
cursor-key: "id-generator:segment:cursor:auth:1" # 断点续传 Key
load-factor: 0.8 # 消耗到 80% 时异步预加载下一号段
ORDER:
step: 5000
key: "id-generator:segment:order"
cursor-key: "id-generator:segment:cursor:order:1"
load-factor: 0.7
| 配置项 | 默认值 | 说明 |
|---|---|---|
step | 10000 | 每次从 Redis 批量申请的 ID 数量,越大访问 Redis 越少,但重启浪费 ID 越多 |
key | id-generator:segment | Redis 号段计数 Key |
cursor-key | id-generator:segment:cursor | 重启断点续传 Key(关闭时写入当前 cursor) |
load-factor | 0.8 | 触发异步预加载的号段消耗比例,范围 (0, 1) |
# 2.4 Java 使用示例
# 生成单个 ID
@Service
public class OrderService {
private final IdGenerator idGenerator;
public OrderService(RedisSegmentIdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
public long createOrder() {
// ID 为正整数,同一号段内严格递增
return idGenerator.nextId(BizType.ORDER);
}
public List<Long> createBatchOrders(int count) {
// 批量生成,适合批量插入场景
return idGenerator.nextIds(BizType.ORDER, count);
}
}
# 手动初始化(非 Spring 容器)
SpringSegmentProperties props = new SpringSegmentProperties();
SpringSegmentProperties.SegmentConfig config = new SpringSegmentProperties.SegmentConfig();
config.setStep(100);
config.setKey("id-generator:segment:test:auth");
config.setCursorKey("id-generator:segment:cursor:test:auth");
config.setLoadFactor(0.8);
props.setConfigs(Map.of(BizType.AUTH.getCode(), config));
RedisSegmentIdGenerator generator = new RedisSegmentIdGenerator(redisUtils, props);
// 必须先初始化
generator.init();
long id = generator.nextId(BizType.AUTH);
List<Long> ids = generator.nextIds(BizType.AUTH, 50);
// 优雅关闭,回写 cursor 到 Redis
generator.close();
# 2.5 测试用例
// 验证 ID 唯一性(500 个 ID 无重复)
@Test
void test_nextId_uniqueness() {
Set<Long> ids = new HashSet<>();
for (int i = 0; i < 500; i++) {
long id = generator.nextId(BizType.AUTH);
Assertions.assertThat(ids.add(id)).isTrue();
}
Assertions.assertThat(ids).hasSize(500);
}
// 验证 ID 单调递增
@Test
void test_nextId_increasing() {
long id1 = generator.nextId(BizType.AUTH);
long id2 = generator.nextId(BizType.AUTH);
Assertions.assertThat(id2).isGreaterThan(id1);
}
// 验证跨号段切换(step=100,生成 250 个会触发 2 次切换)
@Test
void test_nextId_crossSegmentSwitch() {
Set<Long> ids = new HashSet<>();
for (int i = 0; i < 250; i++) {
long id = generator.nextId(BizType.AUTH);
Assertions.assertThat(ids.add(id)).isTrue();
}
Assertions.assertThat(ids).hasSize(250);
}
// 验证批量生成
@Test
void test_nextIds_batch() {
List<Long> ids = generator.nextIds(BizType.AUTH, 50);
Assertions.assertThat(ids).hasSize(50);
Assertions.assertThat(ids).allMatch(id -> id > 0);
Assertions.assertThat(new HashSet<>(ids)).hasSize(50); // 无重复
}
# 2.6 异常行为说明
| 场景 | 异常类型 | 消息 |
|---|---|---|
未调用 init() | IllegalStateException | "not initialized" |
close() 后调用 | IllegalStateException | "not initialized" |
| 等待下一号段超时(约 10s) | RuntimeException | "Timeout waiting for next segment" |
| Redis 连接失败(重试 3 次后) | RuntimeException | "Failed to load segment from Redis after 3 attempts" |
调用 getInstant/getDatacenterId 等 | UnsupportedOperationException | 分段模式不支持雪花ID反解析 |
# 3. 雪花算法 ID 生成器
# 3.1 原理说明
基于 Twitter Snowflake 算法,结合 Nacos 服务注册中心自动分配节点标识,解决集群环境中节点 ID 冲突问题。
# ID 位布局(共 64 位)
┌────┬────────────────────────┬─────────────┬──────────┬─────────────┐
│ 0 │ 时间戳(41位) │ DC(5位) │ M(5位) │ SEQ(13位)│
└────┴────────────────────────┴─────────────┴──────────┴─────────────┘
符号 相对 startTimestamp 的 数据中心ID 机器ID 序列号
位 毫秒偏移
| 字段 | 位数 | 最大值 | 说明 |
|---|---|---|---|
| 符号位 | 1 | — | 固定为 0(确保正整数) |
| 时间戳 | 41 | ~69 年 | 相对于 startTimestamp 的毫秒偏移 |
| datacenterId | 5 | 31 | 数据中心 ID(Nacos 自动分配) |
| machineId | 5 | 31 | 机器 ID(Nacos 自动分配) |
| 序列号 | 13 | 8191 | 同一毫秒内最多 8192 个 ID |
- 最大节点数:32 × 32 = 1024 个
- 单节点每毫秒 TPS:8192
# 3.2 Nacos 节点自动分配流程
启动
└─► 获取 Nacos 所有注册实例的 Metadata(datacenter_id + machine_id)
└─► 找到最小未占用的 (dc, machine) 组合槽位
└─► 将 {datacenter_id, machine_id, grpc_port} 写入本实例 Metadata 并注册
└─► 再次拉取实例列表,检查是否有冲突(并发注册场景)
├── 无冲突 → 初始化成功,订阅实例变更事件
└── 有冲突 → 注销本实例,重新分配(最多重试 10 次)
# 3.3 时钟回拨处理
| 回拨幅度 | 处理方式 |
|---|---|
| ≤ 5ms | 等待 2 倍偏移时间后重新检查,仍回拨则抛异常 |
| > 5ms | 立即抛出 RuntimeException,拒绝生成 ID |
# 3.4 配置说明(application.yml)
spring:
id-generator:
snowflake:
# ⚠️ 起始时间戳,默认 2020-06-15 00:00:00 UTC
# 一旦投产设定,永远不可修改!否则可能产生重复 ID
start-timestamp: 1592150400000
application:
name: laokou-distributed-id-snowflake # Nacos 注册服务名
cloud:
nacos:
discovery:
cluster-name: iot-cluster # Nacos 集群名
grpc:
server:
port: 10111 # gRPC 端口,写入 Nacos 实例 Metadata
# 3.5 Java 使用示例
# 生成 ID
@Service
public class UserService {
private final IdGenerator idGenerator;
public UserService(NacosSnowflakeIdGenerator idGenerator) {
this.idGenerator = idGenerator;
}
// 生成单个 ID
public long createUser() {
return idGenerator.nextId(BizType.AUTH);
}
// 批量生成
public List<Long> createBatchUsers(int count) {
return idGenerator.nextIds(BizType.AUTH, count);
}
// 从 ID 反推生成时间(雪花模式独有)
public Instant getCreateTime(long userId) {
return idGenerator.getInstant(userId);
}
// 从 ID 中拆解各字段(排查问题用)
public void inspectId(long userId) {
long datacenterId = idGenerator.getDatacenterId(userId);
long machineId = idGenerator.getWorkerId(userId);
long sequence = idGenerator.getSequence(userId);
Instant createAt = idGenerator.getInstant(userId);
System.out.printf("ID=%d, dc=%d, machine=%d, seq=%d, time=%s%n",
userId, datacenterId, machineId, sequence, createAt);
}
}
# 手动初始化(非 Spring 容器)
NamingService namingService = NamingUtils.createNamingService("127.0.0.1:8848");
SpringSnowflakeProperties props = new SpringSnowflakeProperties();
// 2021-01-01 00:00:00 UTC
props.setStartTimestamp(1609459200000L);
NacosSnowflakeIdGenerator generator = new NacosSnowflakeIdGenerator(
configManager, serviceManager, props, environment
);
// 向 Nacos 注册,分配 datacenterId + machineId
generator.init();
long id = generator.nextId(BizType.AUTH);
// 取消 Nacos 订阅,清理资源
generator.close();
# 3.6 测试用例
// 验证 1000 个 ID 完全唯一
@Test
void test_nextId_uniqueness() {
Set<Long> ids = new HashSet<>();
for (int i = 0; i < 1000; i++) {
Assertions.assertThat(ids.add(generator.nextId(BizType.AUTH))).isTrue();
}
Assertions.assertThat(ids).hasSize(1000);
}
// 验证 datacenterId 在有效范围 [0, 31]
@Test
void test_getDatacenterId() {
long id = generator.nextId(BizType.AUTH);
Assertions.assertThat(generator.getDatacenterId(id)).isBetween(0L, 31L);
}
// 验证 machineId 在有效范围 [0, 31]
@Test
void test_getWorkerId() {
long id = generator.nextId(BizType.AUTH);
Assertions.assertThat(generator.getWorkerId(id)).isBetween(0L, 31L);
}
// 验证序列号在有效范围 [0, 8191]
@Test
void test_getSequence() {
long id = generator.nextId(BizType.AUTH);
Assertions.assertThat(generator.getSequence(id)).isBetween(0L, 8191L);
}
// 验证从 ID 反推时间(误差在 ±1秒内)
@Test
void test_getInstant() {
Instant before = Instant.now();
long id = generator.nextId(BizType.AUTH);
Instant after = Instant.now();
Instant idInstant = generator.getInstant(id);
Assertions.assertThat(idInstant).isBetween(before.minusSeconds(1), after.plusSeconds(1));
}
# 4. 两种策略对比
| 对比维度 | Redis 分段 | 雪花算法 |
|---|---|---|
| ID 特性 | 趋势递增(号段内严格递增) | 趋势递增(毫秒内序列随机起步) |
| 依赖组件 | Redis | Nacos |
| 时间信息 | ❌ 不含 | ✅ 可从 ID 反推生成时间 |
| 单节点峰值 TPS | 极高(内存自增,无网络开销) | 高(8192 / ms) |
| 最大集群节点数 | 无限制(按 BizType 水平扩展) | 1024 个 |
| 时钟回拨影响 | ❌ 完全不受影响 | ⚠️ >5ms 拒绝生成 |
| 重启 ID 连续性 | ✅ cursor 持久化,断点续传 | ✅ 自然递增 |
| ID 反解析 | ❌ 不支持 | ✅ 可提取时间 / 节点 / 序列 |
| 典型适用场景 | 订单号、业务流水号、数据库主键 | 日志 ID、链路追踪 ID |
# 5. 选型建议
需要从 ID 中获取时间 / 节点信息?
├── 是 → 雪花算法(NacosSnowflakeIdGenerator)
└── 否 → 优先考虑 Redis 分段(RedisSegmentIdGenerator)
ID 更短、更连续,对 B+ 树索引更友好
经验法则:
- 数据库主键 / 业务流水号 → 分段模式(ID 紧凑,写入性能好)
- 分布式日志 / 链路追踪 → 雪花模式(天然携带时间戳,便于排查)
⚠️ 重要:
startTimestamp一旦投产设定,永远不可修改。若修改,在相同毫秒内同一节点可能生成出历史重复 ID。
我是老寇,我们下次再见啦!
上次更新: 3/5/2026, 10:22:08 PM