158 lines
5.4 KiB
Markdown
158 lines
5.4 KiB
Markdown
|
|
---
|
|||
|
|
tags: [microservice, id-generation, snowflake, uuid, distributed-id]
|
|||
|
|
create time: 2026-05-05 12:00
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
# 分布式 ID 生成
|
|||
|
|
|
|||
|
|
## 概述
|
|||
|
|
|
|||
|
|
在微服务架构中,数据库被拆分成多个独立实例,不再共享自增主键。如何生成分布式环境下全局唯一的 ID,是一个经典问题。
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
graph LR
|
|||
|
|
A["❌ 数据库 AUTO_INCREMENT"] -->|"分库后冲突"| Problem["不可用"]
|
|||
|
|
|
|||
|
|
B["✅ 分布式 ID"] --> Unique["全局唯一"]
|
|||
|
|
B --> Ordered["趋势有序"]
|
|||
|
|
B --> HighThroughput["高吞吐"]
|
|||
|
|
|
|||
|
|
style Problem fill:#ffebee
|
|||
|
|
style Unique fill:#e8f5e9
|
|||
|
|
style Ordered fill:#e8f5e9
|
|||
|
|
style HighThroughput fill:#e8f5e9
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
> [!question] 为什么要自己生成?不用数据库自增?
|
|||
|
|
>
|
|||
|
|
> - **分库后**:每个库的自增 ID 从 1 开始,必然冲突
|
|||
|
|
> - **性能瓶颈**:DB 序列或号段模式在高并发下成为瓶颈
|
|||
|
|
> - **耦合度高**:ID 生成依赖特定数据库类型
|
|||
|
|
|
|||
|
|
## 主流方案对比
|
|||
|
|
|
|||
|
|
| 方案 | 唯一性保证 | 有序性 | 性能 (QPS) | 复杂度 | 适用场景 |
|
|||
|
|
|------|-----------|--------|-----------|--------|---------|
|
|||
|
|
| **UUID** | MD5/SHA 哈希保证 | ❌ 完全无序 | ⭐⭐⭐⭐⭐ | 极低 | 临时标识、缓存 Key |
|
|||
|
|
| **雪花算法 (Snowflake)** | WorkerId + 时间戳 | ✅ 趋势有序 | ⭐⭐⭐⭐⭐ | 中 | 通用方案,首选 |
|
|||
|
|
| **号段模式** | DB 批量发放 | ✅ 严格有序 | ⭐⭐⭐⭐ | 中 | 已有 DB 架构改造 |
|
|||
|
|
| **Leaf (美团)** | Snowflake + 号段 | ✅ 趋势/严格有序 | ⭐⭐⭐⭐⭐ | 中高 | 大规模生产环境 |
|
|||
|
|
| **NanoID** | Base62 + CSPRNG | ❌ 无序 | ⭐⭐⭐⭐⭐ | 低 | API Token、短链 |
|
|||
|
|
|
|||
|
|
## 雪花算法 (Snowflake)
|
|||
|
|
|
|||
|
|
Twitter 开源的经典算法,将 ID 分为三部分:
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
64 bit: 1 bit(符号位) + 41 bit(时间戳) + 10 bit(机器ID) + 12 bit(序列号)
|
|||
|
|
│ │ │ │
|
|||
|
|
│ │ │ └─ 每毫秒最多 4096 个 ID
|
|||
|
|
│ │ └──────────────── 1024 台机器
|
|||
|
|
│ └────────────────────────────── ~69 年跨度
|
|||
|
|
└────────────────────────────────────────── 固定为 0
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 核心公式
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
// 伪代码
|
|||
|
|
func generateID() int64 {
|
|||
|
|
timestamp = currentMillis() // 当前毫秒时间戳(相对时间戳)
|
|||
|
|
workerId = 1 // 机器编号 (0-1023)
|
|||
|
|
sequence = incrementSequence() // 递增序号 (0-4095)
|
|||
|
|
|
|||
|
|
return (timestamp << 22) | // 时间戳占高位
|
|||
|
|
(workerId << 12) | // 机器 ID 放中间
|
|||
|
|
sequence // 序列号放在低位
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Go 实现要点
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
type Snowflake struct {
|
|||
|
|
mu sync.Mutex
|
|||
|
|
lastTime int64
|
|||
|
|
sequence int64
|
|||
|
|
workerId int64
|
|||
|
|
workerBits uint8 = 10
|
|||
|
|
stepBits uint8 = 12
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
func (sf *Snowflake) NextID() int64 {
|
|||
|
|
sf.mu.Lock()
|
|||
|
|
defer sf.mu.Unlock()
|
|||
|
|
|
|||
|
|
now := time.Now().UnixMilli()
|
|||
|
|
if now < sf.lastTime {
|
|||
|
|
panic("clock moved backwards") // 时钟回拨保护
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
if now == sf.lastTime {
|
|||
|
|
sf.sequence++
|
|||
|
|
} else {
|
|||
|
|
sf.sequence = 0
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
sf.lastTime = now
|
|||
|
|
|
|||
|
|
return (now << 22) | (sf.workerId << 12) | sf.sequence
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### 注意事项
|
|||
|
|
|
|||
|
|
| 问题 | 解决方案 |
|
|||
|
|
|------|---------|
|
|||
|
|
| **时钟回拨** | 等待到回拨前的时间点再继续;或分配备用 WorkerId |
|
|||
|
|
| **WorkerId 分配** | 启动时从注册中心获取,或使用 K8s Pod Name 映射 |
|
|||
|
|
| **单机 QPS 上限** | 单 WorkerId × 单机房约 409.6 万/秒,通常够用 |
|
|||
|
|
| **跨代际兼容性** | 新字段插入会改变位移量,设计时需预留比特位 |
|
|||
|
|
|
|||
|
|
## UUID v7 (时间有序 UUID)
|
|||
|
|
|
|||
|
|
如果你不想维护 Snowflake 的 WorkerId,可以考虑 **UUID v7**——它是 RFC 9562 定义的新版本,保持了时序性。
|
|||
|
|
|
|||
|
|
```
|
|||
|
|
UUID v7: 48-bit timestamp + 12-bit rand_a + 4-bit version + 12-bit rand_b + 2-bit variant + 62-bit rand_c
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
| 维度 | UUID v4 | UUID v7 |
|
|||
|
|
|------|---------|---------|
|
|||
|
|
| **生成方式** | 纯随机 | 时间戳 + 随机数 |
|
|||
|
|
| **有序性** | ❌ 完全随机 | ✅ 趋势有序 |
|
|||
|
|
| **索引效率** | ❌ 随机插入导致页分裂 | ✅ 顺序插入友好 |
|
|||
|
|
| **存储大小** | 16 bytes | 16 bytes |
|
|||
|
|
| **实现复杂度** | 极低 | 低(标准库支持) |
|
|||
|
|
|
|||
|
|
> [!tip] 如果你的数据库是 MySQL 5.7+,直接改用 `BINARY(16)` 存储 UUID v7,比 BIGINT AUTO_INCREMENT 更优。
|
|||
|
|
|
|||
|
|
## 号段模式
|
|||
|
|
|
|||
|
|
基于数据库分批领取 ID 区间:
|
|||
|
|
|
|||
|
|
```mermaid
|
|||
|
|
sequenceDiagram
|
|||
|
|
App->>DB: SELECT max_id FROM id_generator WHERE biz='order'
|
|||
|
|
DB-->>App: max_id = 10000, step = 2000
|
|||
|
|
|
|||
|
|
Note over App: 内存中维护 [10000, 12000) 号段
|
|||
|
|
|
|||
|
|
loop 每次请求
|
|||
|
|
App->>App: ID++ (本地原子操作)
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
App->>DB: UPDATE id_generator SET max_id = 12000 WHERE max_id = 10000
|
|||
|
|
DB-->>App: ACK
|
|||
|
|
|
|||
|
|
Note over App: 下次取号段: [12000, 14000)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**优点**:兼容现有 MySQL 架构,无需引入外部组件。
|
|||
|
|
**缺点**:存在 ID 浪费(宕机未用完的号段),极端情况下可能产生空洞。
|
|||
|
|
|
|||
|
|
## 关联笔记
|
|||
|
|
|
|||
|
|
- [[03-数据一致性/01-数据库拆分]] — 分库分表场景下的 ID 生成
|
|||
|
|
- [[03-数据一致性/02-分布式事务]] — Outbox 消息 ID 也需要全局唯一
|