easy-idgen ID 生成器组件设计
本文档描述 easy-idgen 组件的设计与实现。该组件提供三种 ID 生成策略:Leaf 号段(leaf-core 外部库)、Snowflake 雪花算法(本地实现)、Simple 简单计数器(本地实现)。三种策略统一到
IdGenerator接口,由IdGeneratorProperties配置驱动 Spring Boot 自动装配,生产代码零外部 MQ/Redis 依赖(Leaf 模式需可选 JDBC 依赖)。
一、背景与问题
1.1 分布式 ID 生成的常见方案对比
| 方案 | 优点 | 缺点 |
|---|---|---|
| 数据库自增 | 简单 | 单点、性能差、分表后冲突 |
| UUID | 全局唯一 | 无序、字符串、长 |
| 雪花算法 Snowflake | 有序、数字、高性能 | 时钟回拨需要处理 |
| Leaf 号段 | 有序、高性能、号段预热 | 需数据库、双 buffer 预热 |
| 号段(自研) | 同 Leaf | 重复造轮子 |
Matrix Framework 选择三种并存:本地单机场景用 Simple,分布式无 DB 场景用 Snowflake,有 DB 场景用 Leaf 号段(依赖 leaf-core 外部库)。
二、组件定位与边界
定位:策略可切换的分布式 ID 门面。业务方注入 IdGenerator,nextId() / nextId(batchSize) 取 ID;底层用配置驱动三种实现之一。
边界:
| 做 | 不做 |
|---|---|
三种策略实现 + IdGenerator 统一接口 | 不做 Redis / Zookeeper 号段实现 |
| Snowflake 本地实现(workerId 从 NetworkInterface 取) | 不做 workerId 中心化注册 |
| Leaf 号段(委托 leaf-core) | 不做 Leaf 号段表 DDL(README 有参考 SQL) |
Spring Boot @ConfigurationProperties + @AutoConfiguration | 不做 Spring Cloud Config 动态刷新 |
| Simple 本地计数器(AtomicLong + 持久化可选) | 不做持久化文件锁 |
约束:
- 雪花算法的
allowClockBackward/maxClockBackwardMs配置字段未生效——时钟回拨阈值硬编码为 5ms(见 关键设计决策) - Leaf 模式运行时强依赖数据库(leaf-core SegmentService),jdbc 依赖为 compile scope
- Snowflake / Simple 模式无外部依赖,本地即可运行
三、总体架构
flowchart TD
subgraph "业务层"
A1["注入 IdGenerator<br/>(接口)"]
end
subgraph "统一接口"
B1["IdGenerator 接口<br/>nextId() / nextId(batchSize)"]
end
subgraph "Spring Boot 自动装配"
C1["EasyIdGenAutoConfiguration<br/>@ConfigurationProperties(IdGeneratorProperties)<br/>@ConditionalOnProperty"]
end
subgraph "三种策略实现"
D1["SnowflakeIdGenerator<br/>雪花算法,本地实现"]
D2["SimpleIdGenerator<br/>AtomicLong + setStartValue + 可选持久化"]
D3["LeafIdGenerator<br/>委托 leaf-core IDGen (外部库)"]
end
subgraph "外部依赖"
E1["leaf-core:1.0.2 (optional)"]
E2["NetworkInterface.getNetworkInterfaces()"]
E3["jdbc:runtime (Leaf 模式)"]
end
A1 --> B1
C1 -- "enabled + type=snowflake" --> D1
C1 -- "enabled + type=simple" --> D2
C1 -- "enabled + type=leaf" --> D3
D3 --> E1
D1 --> E2
D3 --> E3
策略选择流程:
flowchart TD
A["matrix.id-generator.enabled=true"]
B{"type=?"}
C["simple → SimpleIdGenerator<br/>setStartValue(IdGeneratorProperties.simple.startValue)<br/>init()"]
D["snowflake → SnowflakeIdGenerator<br/>注入 SnowflakeProperties<br/>init() 位数校验<br/>generateWorkerId()"]
E["leaf → LeafIdGenerator<br/>segmentService.get(bizTag)<br/>Status.SUCCESS ? getId : throw"]
A --> B
B -- "simple" --> C
B -- "snowflake" --> D
B -- "leaf" --> E
四、三种策略实现对比
| 维度 | Simple | Snowflake | Leaf (leaf-core) |
|---|---|---|---|
| 有序 | ✅ 严格递增 | ✅ 时间戳有序(毫秒) | ✅ 号段内递增 |
| 依赖 | 零 | 零 | leaf-core + JDBC |
| 可分布式 | ❌ 单机(AtomicLong) | ✅ workerId 区分 | ✅ 号段预热 |
| 批量生成 | AtomicLong.addAndGet(batchSize) | 逐毫秒分配,溢出等下一秒 | 从 Segment 取一段 |
| 核心字段 | AtomicLong counter | EPOCH=1672531200000LworkerIdBits=10 / sequenceBits=12 | IDGen.get(bizTag) |
| 初始值 | startValue(默认 1) | EPOCH + workerId 自动 | 数据库 step(默认 1000) |
| 异常处理 | 无 | 时钟回拨 ≥ 5ms → 翻倍等待;batchSize≤0 抛 | Status≠SUCCESS 抛 RuntimeException |
五、包结构与核心类
easy-idgen/
└── io/matrix/genid/
├── IdGenerator.java # 统一接口
├── autoconfig/EasyIdGenAutoConfiguration.java
├── config/
│ ├── IdGeneratorProperties.java # prefix=id-generator
│ ├── SnowflakeProperties.java # workerIdBits=10 / sequenceBits=12
│ └── SimpleGenProperties.java # startValue=1
├── local/
│ ├── SimpleIdGenerator.java # AtomicLong + @PostConstruct init() + reset() + getCurrentValue()
│ └── SnowflakeIdGenerator.java # 雪花算法 + parseId 还原 + 时钟回拨硬编码 5ms
└── leaf/
└── LeafIdGenerator.java # 委托 leaf-core IDGen,@Autowired 注入 segmentService
| 类 | 职责 | 文件 |
|---|---|---|
| IdGenerator | 统一接口:Long nextId() / List<Long> nextId(int batchSize) | IdGenerator.java |
| SimpleIdGenerator | AtomicLong 自增;@PostConstruct init() 从 SnowflakeProperties 读 startValue;batchSize≤0 抛 IllegalArgumentException | SimpleIdGenerator.java |
| SnowflakeIdGenerator | generateWorkerId() 从 NetworkInterface 取 MAC 低 bits;init() 做位数校验(workerIdBits + sequenceBits ≤ 22,各 1-20);时钟回拨阈值硬编码 5ms;parseId 把 id 还原成 timestamp/workerId/sequence | SnowflakeIdGenerator.java |
| LeafIdGenerator | @Autowired private IDGen segmentService(标准 Spring 注入,IDGen Bean 由 leaf-core 自动配置);nextId() 调 segmentService.get(bizTag) 读 Result.getStatus() + getId | LeafIdGenerator.java |
六、雪花算法位分配
默认位分配(可配):
0 ┄ 1 ┄ 41 ┄ 51 ┄ 63
├ timestamp (41 bits) ← 毫秒级时间戳(相对 EPOCH=2023-01-01)
├ workerId (10 bits) ← NetworkInterface MAC 低 bits 取模
└ sequence (12 bits) ← 同毫秒内自增
参数校验(SnowflakeIdGenerator.init()):
| 约束 | 异常 |
|---|---|
| workerIdBits < 1 或 > 20 | IllegalArgumentException |
| sequenceBits < 1 或 > 20 | IllegalArgumentException |
| workerIdBits + sequenceBits > 22 | IllegalArgumentException |
Compact 模式(compactMode=true):强制 workerIdBits=6 / sequenceBits=10,把更多位留给 timestamp。
parseId 还原:
public IdInfo parseId(long id) {
// timestamp = id >> (workerIdBits + sequenceBits) + EPOCH
// workerId = (id >> sequenceBits) & workerIdMask
// sequence = id & sequenceMask
}
七、Spring Boot 自动装配
flowchart TD
A["application.yml"] --> B["matrix.id-generator.enabled=true<br/>matrix.id-generator.snowflake.enabled=true"]
B --> C["EasyIdGenAutoConfiguration<br/>@ConfigurationProperties(prefix='matrix.id-generator')"]
C -- "snowflake.enabled=true" --> D["@Bean SnowflakeIdGenerator<br/>@ConditionalOnProperty(snowflake.enabled=true)"]
C -- "simple.enabled=true" --> E["@Bean SimpleIdGenerator<br/>@ConditionalOnProperty(simple.enabled=true)"]
C -- "segment.enabled=true" --> F["@Bean LeafIdGenerator<br/>@Autowired IDGen segmentService"]
三个 @ConditionalOnProperty:
| Bean | Condition |
|---|---|
| SimpleIdGenerator | matrix.id-generator.simple.enabled=true |
| SnowflakeIdGenerator | matrix.id-generator.snowflake.enabled=true |
| LeafIdGenerator | matrix.id-generator.segment.enabled=true |
八、使用示例
// application.yml
easy:
id-generator:
snowflake:
enabled: true
worker-id-bits: 10
sequence-bits: 12
epoch: 1672531200000 # 2023-01-01
// 注入
@Service
public class OrderService {
@Autowired
private IdGenerator idGenerator;
public Order createOrder(...) {
Long id = idGenerator.nextId();
// 批量生成
List<Long> batch = idGenerator.nextId(10);
return new Order(id, ...);
}
}
// 解析雪花 ID
SnowflakeIdGenerator sf = (SnowflakeIdGenerator) idGenerator;
SnowflakeIdGenerator.IdInfo info = sf.parseId(123456789L);
// info.getTimestamp() → 毫秒时间戳(相对 EPOCH)
// info.getWorkerId() → 节点 ID
// info.getSequence() → 同毫秒内序号
九、关键设计决策
| 决策 | 内容 | 理由 |
|---|---|---|
IdGenerator 接口返回 Long(非原始 long) | 方便 batchSize 边界值处理、null 可扩展 | 但调用方需注意 NPE(当前三种实现永不为 null) |
雪花 EPOCH 硬编码 1672531200000L(2023-01-01) | 减少位空间浪费 | 2023 年前生成的业务 ID 不兼容;README 可注明 |
时钟回拨阈值硬编码 5ms(未用 maxClockBackwardMs 字段) | 简化 v1 实现 | SnowflakeProperties.allowClockBackward / maxClockBackwardMs 在生产代码中从未使用;可作为 v2 修复项 |
| SimpleIdGenerator 的 @PostConstruct init() | 从 SnowflakeProperties 读 startValue | 让 Simple 模式启动时从配置初始化,无需手动 reset() |
LeafIdGenerator @Autowired 注入 IDGen segmentService | leaf-core 的 IDGen 作为 Spring Bean 自动注册到容器 | 如果 leaf-core 自动配置未生效,需要手动定义 IDGen Bean |
| 雪花 workerId 从 NetworkInterface 取 | 本地自发现机制 | MAC 地址低 bits 取模后作为 workerId;生产环境多节点需确保 MAC 不同 |
| batchSize ≤ 0 抛 IllegalArgumentException | fail-fast | 调用方批量生成边界条件 |
十、测试覆盖
当前共 33 例(全绿):
| 测试类 | 用例 | 覆盖点 |
|---|---|---|
| SimpleIdGeneratorTest | 8 | 递增 / batchSize / reset / getCurrentValue / setStartValue + init / batchSize=0 / batchSize=-1 |
| SnowflakeIdGeneratorTest | 13 | 位数校验(<1/>20/>20/>22 和)/ compactMode / nextId 连续递增 / 唯一性 / batchSize / parseId 还原 timestamp+workerId+sequence / IdInfo.toString / conditionMet / conditionNotMet |
| LeafIdGeneratorTest | 6 | 反射注入 IDGen / default bizTag / custom bizTag / Status≠SUCCESS 抛 RuntimeException / batchSize=0 / batchSize=-1 |
| IdGeneratorPropertiesTest | 3 | enabled 默认 true / database 默认驱动 / segment 配置 |
| SimpleGenPropertiesTest | 2 | 默认值 / setter |
| SnowflakePropertiesTest | 1 | 默认值(workerIdBits=10 / sequenceBits=12 / epoch=null / allowClockBackward=true / maxClockBackwardMs=5) |
可测程度 4/5:SimpleIdGenerator / SnowflakeIdGenerator 纯 JVM;LeafIdGenerator 依赖 leaf-core Mock 但易 mock(Result/Status 都是 POJO)。Snowflake 时钟回拨阈值硬编码 5ms——不测试 maxClockBackwardMs 字段(已知 bug)。
踩坑:
StateMachineException— COLA StateMachineFactory 不允许重复 idResult构造器参数顺序 — leaf-coreResult(long id, Status status)而非(Status, long)- Mock
withContext(Runnable)— mock default 方法不会自动调用 Runnable,需doAnswer(inv -> inv.getArgument(0, Runnable.class).run()) - 雪花 clock backward 硬编码 5ms —
maxClockBackwardMs字段不生效,测试不覆盖