第 13 章:分布式 ID
学习目标
- 理解分布式 ID 的核心要求:全局唯一、趋势递增
- 掌握雪花算法(Snowflake)的实现
- 学会号段模式和 Leaf 方案
- 避免时间回拨、ID 重复的坑
一、为什么不用数据库自增?
text
单库时代
[订单表] → id = 1, 2, 3, 4... ✅ 没问题
分库分表后
[订单库 0] → id = 1, 3, 5, 7... (奇数)
[订单库 1] → id = 2, 4, 6, 8... (偶数)
每个库自增不同 = 重复 ❌分布式 ID 三大要求:
- 全局唯一:跨库、跨表、跨服务
- 高性能:每秒生成数万 ID
- 趋势递增:主键排序、MySQL 索引(B+ 树追加,避免页分裂)
二、UUID
java
// 简单方案
String id = UUID.randomUUID().toString();
// 550e8400-e29b-41d4-a716-446655440000text
优点:本地生成,无依赖
缺点:无序(36 字符串,占空间)、不可读、不递增java
// 数据库表设计
CREATE TABLE order (
id BINARY(16) PRIMARY KEY, -- UUID 二进制存
-- ...
);⚠️ 坑 1:UUID 当主键,MySQL InnoDB 主键聚簇索引,UUID 无序 → 频繁页分裂,插入性能差。
三、雪花算法(Snowflake)
Twitter 2010 年提出,64 bit 整数:
text
| 1 bit 符号 | 41 bit 时间戳 | 10 bit 机器 | 12 bit 序列 |
| 0 | 当前毫秒 | 1024 节点 | 4096/ms |java
public class SnowflakeIdGenerator {
private static final long EPOCH = 1700000000000L; // 起始时间戳
private static final long MACHINE_BITS = 10;
private static final long SEQUENCE_BITS = 12;
private final long machineId;
private long lastTimestamp = -1L;
private long sequence = 0;
private final Object lock = new Object();
public SnowflakeIdGenerator(long machineId) {
if (machineId < 0 || machineId >= (1 << MACHINE_BITS)) {
throw new IllegalArgumentException("机器 ID 超出范围");
}
this.machineId = machineId;
}
public long nextId() {
synchronized (lock) {
long timestamp = System.currentTimeMillis();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,等待" + (lastTimestamp - timestamp) + "ms");
}
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);
if (sequence == 0) {
// 同一毫秒序列号满,等下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
return ((timestamp - EPOCH) << (MACHINE_BITS + SEQUENCE_BITS))
| (machineId << SEQUENCE_BITS)
| sequence;
}
}
private long waitNextMillis(long last) {
long ts = System.currentTimeMillis();
while (ts <= last) {
ts = System.currentTimeMillis();
}
return ts;
}
}java
// 使用
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1); // 机器 ID = 1
for (int i = 0; i < 10; i++) {
System.out.println(generator.nextId());
}
// 734981723456000001 ...⚠️ 坑 2:时钟回拨(系统时间被改到过去)雪花会抛异常。生产机器禁 NTP 自动同步,或代码里等回拨结束。
四、美团 Leaf
java
// 号段模式:每次从 DB 取一段 ID
public class LeafSegment {
private final AtomicLong currentId;
private final long maxId;
private final String key;
private final SegmentDao dao;
public LeafSegment(String key, SegmentDao dao) {
this.key = key;
this.dao = dao;
Segment s = dao.loadSegment(key);
this.currentId = new AtomicLong(s.getMinId());
this.maxId = s.getMaxId();
}
public long nextId() {
long id = currentId.incrementAndGet();
if (id > maxId) {
// 异步加载下一段
synchronized (this) {
if (currentId.get() > maxId) {
Segment next = dao.loadNextSegment(key);
currentId.set(next.getMinId());
// ...
}
}
}
return id;
}
}sql
-- 数据库表
CREATE TABLE leaf_alloc (
biz_tag VARCHAR(50) PRIMARY KEY,
max_id BIGINT NOT NULL,
step INT NOT NULL, -- 步长,默认 1000
update_time TIMESTAMP NOT NULL
);五、百度 UidGenerator
java
// 基于 Snowflake 改进
// 1. 用未来时间解决时钟回拨
// 2. 借助 DB 分配 workId
// 3. RingBuffer 预生成 IDjava
@Service
public class UidGeneratorService {
@Resource
private DefaultUidGenerator uidGenerator;
public long nextId() {
return uidGenerator.getUID();
}
public String nextIdStr() {
return uidGenerator.getUIDStr();
}
}六、ID 性能与方案选择
| 方案 | TPS | 趋势递增 | 长度 | 场景 |
|---|---|---|---|---|
| UUID | 极高 | 无 | 32 | 临时 ID |
| DB 自增 | 1000 | ✅ | 8 | 单库 |
| 雪花 | 10万+ | ✅ | 8 | 通用 |
| Leaf 号段 | 10万+ | ✅ | 8 | 高并发 |
| Leaf 雪花 | 10万+ | ✅ | 8 | 长期 |
七、ID 编码可读性
java
// 业务前缀:ORD + 雪花 ID
public class OrderId {
private final long value;
public OrderId(long value) {
this.value = value;
}
public String encoded() {
return "ORD" + value;
}
public String toString() {
return encoded();
}
}
// 用户前缀:USER
public class UserId {
public String encoded() {
return "USER" + value;
}
}⚠️ 坑 3:日志里看到
id=789123456789012345,不知道是订单还是用户。前缀化让日志一眼可读。
八、ID 发送的可靠性
java
// Worker 模式:批量预生成,异步填充
public class IdWorker {
private final BlockingQueue<Long> queue = new LinkedBlockingQueue<>(10000);
@PostConstruct
public void init() {
new Thread(() -> {
while (true) {
if (queue.size() < 1000) {
for (int i = 0; i < 1000; i++) {
queue.offer(snowflake.nextId());
}
}
Thread.sleep(10);
}
}).start();
}
public long nextId() {
return queue.poll();
}
}本章小结
| 方案 | 特点 |
|---|---|
| UUID | 简单,无序,占空间 |
| Snowflake | 高性能,趋势递增,时钟敏感 |
| Leaf 号段 | 稳定,DB 持久化 |
| Leaf 雪花 | 雪花的优化 |
| 关键点 | 建议 |
|---|---|
| 业务前缀 | ORD/USER/PAY |
| 长度 | 64 bit,Long 类型 |
| 时钟 | 禁 NTP 自动同步 |
| 重复检测 | 唯一索引兜底 |
动手练习
- 雪花实现:写一个
SnowflakeIdGenerator,生成 100 万 ID 验证唯一 - Leaf 接入:用美团 Leaf 在 Spring Boot 中集成号段模式
- 时钟回拨:模拟时钟回拨,看雪花是否抛异常
- 业务前缀:给订单 ID 加
ORD前缀,验证可读性
下一章:第 14 章:分布式事务 →