Files
interview/questions/01-分布式系统/分布式ID生成.md

732 lines
18 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 分布式 ID 生成方案
## 问题
1. 为什么需要分布式 ID分布式 ID 有哪些要求?
2. 常见的分布式 ID 生成方案有哪些?各自的优缺点是什么?
3. Snowflake 算法的原理是什么?有什么坑?
4. 数据库自增 ID 如何实现分布式?如何优化性能?
5. Redis 如何生成分布式 ID有哪些优缺点
6. 在实际项目中,你是如何设计分布式 ID 的?
---
## 标准答案
### 1. 分布式 ID 的要求和特点
#### **为什么需要分布式 ID**
在分布式系统中,需要保证 ID 的**全局唯一性**
- 订单号、用户 ID、支付流水号
- 分库分表后的主键冲突问题
- 微服务间的数据关联
**单机 ID 的问题**
```
单机数据库自增 ID
- 实例 11, 2, 3, 4, 5
- 实例 21, 2, 3, 4, 5 ← 冲突!
```
---
#### **分布式 ID 的核心要求**
| 要求 | 说明 | 示例 |
|------|------|------|
| **全局唯一性** | 不能重复 | 订单号不能重复 |
| **有序性** | 趋势递增(可选) | 按时间排序的订单号 |
| **高性能** | 生成速度快 | 支持 10 万+/秒 |
| **高可用** | 服务不中断 | 宕机后仍可生成 |
| **信息安全** | 不暴露业务信息 | 不暴露订单总量 |
---
### 2. 常见分布式 ID 方案对比
| 方案 | 唯一性 | 有序性 | 性能 | 复杂度 | 适用场景 |
| -------------- | --- | --- | ----- | --- | --------- |
| **UUID** | ✅ | ❌ | ⭐⭐⭐⭐⭐ | 低 | 非主键、内部 ID |
| **数据库自增** | ✅ | ✅ | ⭐⭐ | 低 | 小规模、并发低 |
| **Redis INCR** | ✅ | ✅ | ⭐⭐⭐ | 低 | 中小规模 |
| **Snowflake** | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 中 | 大规模、高并发 |
| **号段模式** | ✅ | ✅ | ⭐⭐⭐⭐ | 中 | 大规模、高性能 |
| **美团 Leaf** | ✅ | ✅ | ⭐⭐⭐⭐⭐ | 高 | 金融级、高可用 |
---
### 3. UUID
#### **原理**
UUIDUniversally Unique Identifier是 128 位的唯一标识符。
**格式**
```
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
550e8400-e29b-41d4-a716-446655440000
```
**Java 示例**
```java
import java.util.UUID;
String uuid = UUID.randomUUID().toString();
// 输出550e8400-e29b-41d4-a716-446655440000
```
---
#### **优缺点**
**优点**
- 性能高:本地生成,无网络开销
- 简单JDK 自带,无需额外组件
**缺点**
- **无序**:无法按时间排序
- **过长**36 字符,存储空间大
- **不安全**:暴露 MAC 地址UUID v1
- **索引性能差**:无序 ID 导致 B+ 树频繁分裂
**B+ 树分裂问题**
```
有序 ID
1 → 100 → 1000 → 10000
└─ 顺序插入B+ 树叶子节点顺序填充
无序 IDUUID
abc → xyz → 123 → 999
└─ 随机插入B+ 树频繁分裂,性能差
```
---
#### **适用场景**
- ✅ 非数据库主键(如请求 ID
- ✅ 临时标识、会话 ID
- ✅ 内部系统、不需要排序
- ❌ 订单号、用户 ID需要有序
---
### 4. 数据库自增 ID
#### **方案 1单机自增**
```sql
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32),
created_at DATETIME
);
```
**问题**:单机性能瓶颈,无法扩展。
---
#### **方案 2多机步长模式Flickr 方案)**
**原理**:不同数据库实例设置不同的起始值和步长。
```
实例 1起始 1步长 2 → 1, 3, 5, 7, 9
实例 2起始 2步长 2 → 2, 4, 6, 8, 10
```
**配置**
```sql
-- 实例 1
SET auto_increment_increment = 2;
SET auto_increment_offset = 1;
-- 实例 2
SET auto_increment_increment = 2;
SET auto_increment_offset = 2;
```
**优点**
- 实现简单
- ID 有序
**缺点**
- 扩容困难:需要重新计算步长
- 性能瓶颈:数据库写入限制
---
#### **方案 3号段模式批量获取**
**原理**:一次从数据库获取一批 ID号段缓存在本地。
**表结构**
```sql
CREATE TABLE id_segment (
biz_type VARCHAR(32) PRIMARY KEY, -- 业务类型
max_id BIGINT, -- 当前最大 ID
step INT, -- 步长(批量大小)
version INT, -- 版本号(乐观锁)
updated_at DATETIME
);
-- 初始化数据
INSERT INTO id_segment (biz_type, max_id, step, version)
VALUES ('order', 0, 1000, 1);
```
**获取 ID 逻辑**
```java
@Service
public class IdSegmentService {
@Autowired
private IdSegmentMapper segmentMapper;
private final Map<String, IdSegment> localCache = new ConcurrentHashMap<>();
@Transactional
public synchronized Long nextId(String bizType) {
IdSegment segment = localCache.get(bizType);
// 本地号段用完,从数据库获取
if (segment == null || segment.getCurrentId() >= segment.getMaxId()) {
segment = fetchSegmentFromDb(bizType);
}
// 返回下一个 ID
return segment.getNextId();
}
private IdSegment fetchSegmentFromDb(String bizType) {
// 使用 CAS 更新数据库
IdSegment dbSegment = segmentMapper.selectByType(bizType);
// 更新 max_id = max_id + step
int updated = segmentMapper.updateMaxId(
bizType,
dbSegment.getMaxId() + dbSegment.getStep(),
dbSegment.getVersion()
);
if (updated == 0) {
throw new RuntimeException("并发冲突,请重试");
}
// 缓存到本地
IdSegment newSegment = new IdSegment();
newSegment.setMaxId(dbSegment.getMaxId() + dbSegment.getStep());
newSegment.setCurrentId(dbSegment.getMaxId());
localCache.put(bizType, newSegment);
return newSegment;
}
}
```
**Mapper**
```java
@Update("UPDATE id_segment SET max_id = #{maxId}, version = version + 1 " +
"WHERE biz_type = #{bizType} AND version = #{version}")
int updateMaxId(@Param("bizType") String bizType,
@Param("maxId") Long maxId,
@Param("version") Integer version);
```
**优点**
- 性能高:本地缓存,减少数据库访问
- 有序ID 趋势递增
**缺点**
- 宕机丢 ID本地缓存的 ID 未使用完就丢失
- 实现复杂
**优化**使用双缓冲Double Buffer机制预加载号段。
---
### 5. Redis INCR
#### **原理**
使用 Redis 的 `INCR``INCRBY` 命令生成全局唯一 ID。
**示例**
```bash
# 初始化
SET order:id 1
# 获取下一个 ID
INCR order:id
# 返回2
# 批量获取(步长 1000
INCRBY order:id 1000
# 返回1001
```
---
#### **Java 实现**
```java
@Service
public class RedisIdGenerator {
@Autowired
private StringRedisTemplate redisTemplate;
public Long nextId(String key) {
// 使用 redisTemplate 的 opsForValue
Long id = redisTemplate.opsForValue().increment(key);
if (id == null) {
throw new RuntimeException("生成 ID 失败");
}
return id;
}
// 批量获取(优化性能)
public Long[] batchNextId(String key, int count) {
Long startId = redisTemplate.opsForValue().increment(key, count);
Long[] ids = new Long[count];
for (int i = 0; i < count; i++) {
ids[i] = startId - count + i + 1;
}
return ids;
}
}
```
---
#### **优缺点**
**优点**
- 性能高Redis 内存操作
- 有序ID 趋势递增
- 实现简单
**缺点**
- 依赖 Redis需要维护 Redis 集群
- 宕机丢数据:未持久化会丢失
**持久化配置**
```properties
# 开启 AOF 持久化
appendonly yes
appendfsync everysec
```
---
#### **适用场景**
- 中小规模、性能要求高
- 已有 Redis 集群
- 可容忍短期 ID 丢失
---
### 6. Snowflake 算法
#### **原理**
Snowflake 是 Twitter 开源的分布式 ID 算法,生成 64 位的 Long 型 ID。
**结构**64 位):
```
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
↑ ↑ ↑ ↑ ↑
│ │ │ │ │
│ └─ 41 位时间戳(毫秒) │ │ │
│ │ │ │
│ └─ 5 位数据中心 ID │
│ └─ 12 位序列号
1 位符号位(始终为 0
```
**组成部分**
- **1 位符号位**:始终为 0正数
- **41 位时间戳**:毫秒级,可用 69 年(`2^41 / 1000 / 60 / 60 / 24 / 365 ≈ 69`
- **5 位数据中心 ID**:支持 32 个数据中心
- **5 位机器 ID**:每个数据中心 32 台机器
- **12 位序列号**:每毫秒可生成 4096 个 ID
**理论 QPS**
```
单机4096 / 毫秒 = 409.6 万/秒
```
---
#### **Java 实现**
```java
public class SnowflakeIdGenerator {
// 起始时间戳2024-01-01 00:00:00
private final long twepoch = 1704067200000L;
// 各部分位数
private final long workerIdBits = 5L;
private final long datacenterIdBits = 5L;
private final long sequenceBits = 12L;
// 最大值
private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
private final long maxSequence = -1L ^ (-1L << sequenceBits); // 4095
// 位移
private final long workerIdShift = sequenceBits; // 12
private final long datacenterIdShift = sequenceBits + workerIdBits; // 17
private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits; // 22
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long workerId, long datacenterId) {
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException("workerId 无效");
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException("datacenterId 无效");
}
this.workerId = workerId;
this.datacenterId = datacenterId;
}
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨检查
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成 ID");
}
// 同一毫秒内,序列号自增
if (timestamp == lastTimestamp) {
sequence = (sequence + 1) & maxSequence;
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
// 新毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装 ID
return ((timestamp - twepoch) << timestampShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
// 等待下一毫秒
private long tilNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
// 解析 ID用于调试
public static void parseId(long id) {
long timestamp = (id >> 22) + 1704067200000L;
long datacenterId = (id >> 17) & 0x1F;
long workerId = (id >> 12) & 0x1F;
long sequence = id & 0xFFF;
System.out.println("ID: " + id);
System.out.println("时间戳: " + new Date(timestamp));
System.out.println("数据中心 ID: " + datacenterId);
System.out.println("机器 ID: " + workerId);
System.out.println("序列号: " + sequence);
}
}
```
**使用示例**
```java
// 初始化workerId=1, datacenterId=1
SnowflakeIdGenerator idGenerator = new SnowflakeIdGenerator(1, 1);
// 生成 ID
long id = idGenerator.nextId();
System.out.println("生成的 ID: " + id);
// 解析 ID
SnowflakeIdGenerator.parseId(id);
```
---
#### **Snowflake 的坑**
##### **问题 1时钟回拨**
**原因**
- 系统时钟不准确NTP 同步)
- 人工修改系统时间
**后果**
```
时间10:00:00.100,生成 ID时间戳=T1
时钟回拨:时间 → 09:59:59.900
时间09:59:59.950,生成 ID时间戳=T2 < T1← 冲突!
```
**解决方案**
1. **拒绝服务**(简单):
```java
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成 ID");
}
```
2. **等待时钟追上**(推荐):
```java
if (timestamp < lastTimestamp) {
long offset = lastTimestamp - timestamp;
if (offset <= 5) {
// 等待 5 毫秒
try {
Thread.sleep(offset << 1);
timestamp = System.currentTimeMillis();
} catch (InterruptedException e) {
throw new RuntimeException("时钟回拨,等待失败");
}
} else {
throw new RuntimeException("时钟回拨过多,拒绝生成 ID");
}
}
```
3. **使用备用 workerId**(美团 Leaf 方案):
```java
if (timestamp < lastTimestamp) {
// 切换到备用 workerId
workerId = backupWorkerId;
}
```
---
##### **问题 2机器 ID 分配**
**问题**:如何保证 workerId 全局唯一?
**解决方案**
1. **配置文件**(简单):
```yaml
application.yml:
snowflake:
worker-id: 1
datacenter-id: 1
```
2. **数据库配置**
```sql
CREATE TABLE worker_config (
id INT PRIMARY KEY,
worker_id INT,
datacenter_id INT,
ip VARCHAR(32),
used BOOLEAN
);
-- 启动时申请 workerId
INSERT INTO worker_config (worker_id, datacenter_id, ip, used)
VALUES (1, 1, '192.168.1.10', TRUE);
```
3. **Zookeeper 顺序节点**(动态):
```java
// 在 Zookeeper 中创建临时顺序节点
String path = zk.create("/snowflake/worker-", null, ZooDefs.Ids.EPHEMERAL_SEQUENTIAL);
// 获取序号作为 workerId
int workerId = Integer.parseInt(path.split("-")[1]);
```
4. **Redis INCR**(动态):
```java
Long workerId = redisTemplate.opsForValue().increment("snowflake:worker:id");
```
---
##### **问题 3序列号溢出**
**场景**高并发下1 毫秒内请求超过 4096 个。
**解决**
```java
// 序列号溢出,等待下一毫秒
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
```
**优化**:使用 13 位序列号(每毫秒 8192 个 ID
---
#### **优缺点**
**优点**
- 性能极高409 万 QPS/单机
- 有序:趋势递增(按时间排序)
- 不依赖数据库、Redis
**缺点**
- 时钟回拨问题
- 机器 ID 分配复杂
- ID 较长18 位数字)
---
#### **适用场景**
- 大规模、高并发场景
- 需要有序 ID
- 可容忍时钟问题(或已有解决方案)
**实际应用**
- 百度 UidGenerator
- 美团 LeafSnowflake 模式)
- Etsy
---
### 7. 美团 Leaf
#### **Leaf-segment号段模式**
**原理**:优化版号段模式,使用双缓冲机制。
**架构**
```
Leaf Server
├─ Buffer 1当前使用号段 [1000, 2000)
└─ Buffer 2预加载号段 [2000, 3000)(后台异步加载)
当 Buffer 1 用完:
└─ 切换到 Buffer 2
└─ 异步加载 Buffer 1 的下一个号段
```
**优点**
- 无停顿:双缓冲无缝切换
- 高性能:本地缓存
**缺点**
- 宕机丢 ID未使用的号段丢失
---
#### **Leaf-snowflake优化版 Snowflake**
**优化点**
1. **Zookeeper 生成 workerId**:动态分配,无需配置
2. **时钟回拨优化**
- 回拨 5ms 内:等待时钟追上
- 回拨 5ms 外:告警并拒绝服务
**架构**
```
Leaf Server 1workerId=1
Leaf Server 2workerId=2
Leaf Server 3workerId=3
Zookeeper协调
```
**GitHub**https://github.com/Meituan-Dianping/Leaf
---
### 8. 百度 UidGenerator
**特点**
- 基于 Snowflake 优化
- 使用 22 位序列号(每秒 400 万 ID
- 支持跨毫秒分配序列号
**GitHub**https://github.com/baidu/uid-generator
---
### 9. 实际项目选型建议
#### **决策树**
```
是否需要有序?
├─ 否 → UUID最简单
└─ 是 → 继续判断
├─ QPS < 1000
│ ├─ 是 → Redis INCR简单
│ └─ 否 → 继续判断
├─ 已有 Redis
│ ├─ 是 → 号段模式(高性能)
│ └─ 否 → 继续判断
├─ 可容忍时钟回拨问题?
│ ├─ 是 → Snowflake性能最高
│ └─ 否 → 美团 Leaf-snowflake
└─ 金融级可靠性?
└─ 美团 Leaf-segment + 监控
```
---
#### **性能对比**
| 方案 | 单机 QPS | 延迟 | 依赖 |
|------|---------|------|------|
| UUID | 1000 万+ | 0.001ms | 无 |
| 数据库自增 | 1000 | 10ms | 数据库 |
| Redis INCR | 10 万 | 1ms | Redis |
| 号段模式 | 100 万 | 0.1ms | 数据库 |
| Snowflake | 400 万 | 0.01ms | 无 |
---
### 10. 阿里 P7 加分项
**深度理解**
- 理解 Snowflake 的时间戳回拨问题的根本原因
- 理解号段模式的双缓冲机制和 CAS 原理
**实战经验**
- 有处理 Snowflake 时钟回拨的线上故障经验
- 有号段模式宕机丢 ID 的解决方案
- 有分布式 ID 迁移经验(如从数据库自增迁移到 Snowflake
**架构能力**
- 能设计支持多业务类型的分布式 ID 系统
- 能设计分布式 ID 的监控和告警体系
- 有分布式 ID 容灾方案(多机房容灾)
**技术选型**
- 能根据业务特点选择合适的方案
- 了解美团 Leaf、百度 UidGenerator 等开源方案
- 有自研分布式 ID 生成器的经验