分布式 ID 生成
为什么需要分布式 ID?
在分布式系统中,单机数据库的自增 ID 无法保证全局唯一,需要专门的 ID 生成方案。
| 要求 | 说明 |
|---|---|
| 全局唯一 | 分布式环境下 ID 不重复 |
| 趋势递增 | 利于数据库索引 |
| 高性能 | 高并发下快速生成 |
| 高可用 | ID 生成服务稳定可靠 |
常见方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| UUID | 简单、本地生成 | 无序、过长、索引性能差 |
| 数据库自增 | 简单、递增 | 单点问题、性能瓶颈 |
| 号段模式 | 高性能、递增 | 需要数据库 |
| Snowflake | 高性能、递增 | 时钟回拨问题 |
| Redis | 高性能 | 依赖 Redis |
Snowflake(雪花算法)
结构
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
│ └─────────────────── 41位时间戳 ───────────────────┘ │ │ │
│ │ │ └── 12位序列号
│ │ └── 5位机器ID
│ └── 5位数据中心ID
└── 1位符号位(始终为0)
总计:64位 = 8字节 = long 类型特点
| 部分 | 位数 | 说明 |
|---|---|---|
| 符号位 | 1 | 始终为 0 |
| 时间戳 | 41 | 毫秒级,可用 69 年 |
| 数据中心 | 5 | 最多 32 个数据中心 |
| 机器 ID | 5 | 每个数据中心最多 32 台机器 |
| 序列号 | 12 | 每毫秒最多 4096 个 ID |
代码实现
public class SnowflakeIdGenerator {
// 起始时间戳(2020-01-01)
private final long twepoch = 1577836800000L;
// 位数分配
private final long datacenterIdBits = 5L;
private final long workerIdBits = 5L;
private final long sequenceBits = 12L;
// 最大值
private final long maxDatacenterId = ~(-1L << datacenterIdBits);
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long sequenceMask = ~(-1L << sequenceBits);
// 位移
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
private long datacenterId;
private long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long datacenterId, long workerId) {
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId 越界");
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId 越界");
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
// 同一毫秒内
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 序列号用完,等待下一毫秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
}时钟回拨问题
问题:服务器时钟回拨,可能生成重复 ID
解决方案:
1. 等待时钟追上(简单但影响性能)
2. 使用 Zookeeper 管理机器 ID
3. 记录上次生成时间,回拨时抛异常
4. 百度/美团的优化方案号段模式
原理
数据库表:
┌───────┬───────────┬──────────┐
│ tag │ max_id │ step │
├───────┼───────────┼──────────┤
│ order │ 1000 │ 1000 │
└───────┴───────────┴──────────┘
1. 服务启动,查询 max_id = 1000, step = 1000
2. 本地生成 [1000, 2000) 号段
3. 异步更新数据库 max_id = 2000
4. 号段用完,获取新号段代码实现
@Service
public class SegmentIdService {
@Autowired
private JdbcTemplate jdbcTemplate;
private AtomicLong currentId = new AtomicLong(0);
private AtomicLong maxId = new AtomicLong(0);
private final int step = 1000;
public synchronized long nextId() {
// 号段用完,获取新号段
if (currentId.get() >= maxId.get()) {
fetchNewSegment();
}
return currentId.getAndIncrement();
}
private void fetchNewSegment() {
// 更新数据库
jdbcTemplate.update(
"UPDATE id_segment SET max_id = max_id + ? WHERE tag = 'order'",
step
);
// 查询新的 max_id
Long newMaxId = jdbcTemplate.queryForObject(
"SELECT max_id FROM id_segment WHERE tag = 'order'",
Long.class
);
// 设置本地号段
currentId.set(newMaxId - step);
maxId.set(newMaxId);
}
}双 Buffer 优化
Buffer A: [1000, 2000) ← 当前使用
Buffer B: [2000, 3000) ← 预加载
当 Buffer A 使用达到 20% 时,异步加载 Buffer B
切换时无需等待,性能更高美团 Leaf
美团开源的分布式 ID 生成系统,提供两种模式:
| 模式 | 说明 |
|---|---|
| Leaf-segment | 号段模式,适合订单 ID |
| Leaf-snowflake | Snowflake 优化,适合消息 ID |
Leaf-segment 特性
- 双 Buffer 优化
- 数据库高可用
- 号段预加载
Leaf-snowflake 特性
- Zookeeper 管理机器 ID
- 解决时钟回拨问题
方案对比
| 方案 | 性能 | 递增 | 依赖 | 适用场景 |
|---|---|---|---|---|
| UUID | 高 | 否 | 无 | 无需排序的场景 |
| 数据库自增 | 低 | 是 | 数据库 | 低并发场景 |
| Snowflake | 高 | 是 | 无(需解决时钟问题) | 高并发场景 |
| 号段模式 | 高 | 是 | 数据库 | 订单 ID |
| Redis | 高 | 是 | Redis | 分布式环境 |
面试高频问题
Q1: Snowflake 的原理?
64 位 = 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号
Q2: Snowflake 如何解决时钟回拨?
- 记录上次时间戳,回拨时等待或抛异常
- 使用 Zookeeper 管理机器 ID
- 美团 Leaf 方案:回拨时使用备用 workerId
Q3: 号段模式的优势?
- 高性能:本地生成,不依赖网络
- 趋势递增:利于数据库索引
- 高可用:双 Buffer 预加载
Q4: 为什么不用 UUID?
- 无序:不利于数据库索引
- 过长:32 个字符,存储空间大
- 无业务含义:无法从 ID 中提取信息
总结
分布式 ID 核心要点:
1. Snowflake:41 位时间戳 + 10 位机器 + 12 位序列
2. 号段模式:批量获取号段,本地生成
3. 推荐:美团 Leaf(开源实现)
4. 注意:时钟回拨、高可用