如何设计分布式ID生成器
一、问题描述
1.1 业务背景
在分布式系统中,需要为订单、用户、消息等对象生成全局唯一的ID。传统的MySQL自增ID在分布式场景下无法满足需求,需要专门的分布式ID生成方案。
应用场景:
- 订单系统:生成订单号、支付单号
- 用户系统:生成用户ID、设备ID
- 消息系统:生成消息ID、会话ID
- 日志系统:生成追踪ID(Trace ID)
1.2 核心需求
必须满足:
- 全局唯一:绝对不能重复
- 高性能:QPS达到10万+
- 高可用:故障后快速恢复
最好满足: 4. 趋势递增:方便MySQL索引 5. 信息安全:不暴露业务信息 6. 可反解:能解析出时间戳、机器ID等
1.3 技术挑战
唯一性保证:
- 如何在分布式环境下保证唯一
- 时钟回拨如何处理
性能要求:
- 单机QPS需要10万+
- 响应延迟<1ms
高可用性:
- 单点故障如何处理
- 如何实现无状态
1.4 面试考察点
- 算法原理:雪花算法、美团Leaf等
- 时钟回拨:如何处理时钟回拨
- 唯一性保证:如何保证绝对唯一
- 性能优化:如何达到10万+QPS
- 高可用:如何实现故障转移
二、方案对比
2.1 常见方案对比
| 方案 | 优点 | 缺点 | QPS | 适用场景 |
|---|---|---|---|---|
| UUID | 简单、本地生成 | 无序、36字符长 | 无限 | 不关心顺序 |
| MySQL自增 | 简单、递增 | 性能差、单点 | 1000 | 小规模系统 |
| Redis | 高性能 | 依赖Redis、持久化问题 | 10万 | 临时ID |
| 雪花算法 | 高性能、趋势递增 | 依赖时钟 | 100万 | 推荐 |
| 美团Leaf | 高可用、双Buffer | 复杂度高 | 10万 | 大规模系统 |
| 百度UidGenerator | 性能极高 | 复杂度高 | 600万 | 超大规模 |
2.2 雪花算法(Snowflake)
结构(64位long):
0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
| |-------------------------------------------| |-----| |-----| |----------|
1位 41位时间戳(毫秒) 5位 5位 12位序列号
未用 (可用69年) 机房ID 机器ID (单机每毫秒4096个)优点:
- 高性能:本地生成,QPS可达100万+
- 趋势递增:适合MySQL索引
- 信息可解析:包含时间戳、机器ID
缺点:
- 依赖时钟:时钟回拨会导致重复
- 依赖机器ID:需要配置管理
三、核心实现
3.1 雪花算法(Go实现)
点击查看完整实现
go
package snowflake
import (
"errors"
"sync"
"time"
)
const (
// 时间戳占41位
timestampBits = 41
// 机房ID占5位
datacenterIDBits = 5
// 机器ID占5位
workerIDBits = 5
// 序列号占12位
sequenceBits = 12
// 最大值
maxDatacenterID = -1 ^ (-1 << datacenterIDBits) // 31
maxWorkerID = -1 ^ (-1 << workerIDBits) // 31
maxSequence = -1 ^ (-1 << sequenceBits) // 4095
// 位移
workerIDShift = sequenceBits // 12
datacenterIDShift = sequenceBits + workerIDBits // 17
timestampShift = sequenceBits + workerIDBits + datacenterIDBits // 22
// 时间起点(2020-01-01 00:00:00)
epoch int64 = 1577808000000
)
// Snowflake ID生成器
type Snowflake struct {
mu sync.Mutex
timestamp int64 // 上次生成ID的时间戳
datacenterID int64 // 机房ID
workerID int64 // 机器ID
sequence int64 // 序列号
}
// NewSnowflake 创建Snowflake生成器
func NewSnowflake(datacenterID, workerID int64) (*Snowflake, error) {
if datacenterID < 0 || datacenterID > maxDatacenterID {
return nil, errors.New("datacenterID must be between 0 and 31")
}
if workerID < 0 || workerID > maxWorkerID {
return nil, errors.New("workerID must be between 0 and 31")
}
return &Snowflake{
timestamp: 0,
datacenterID: datacenterID,
workerID: workerID,
sequence: 0,
}, nil
}
// NextID 生成下一个ID
func (s *Snowflake) NextID() (int64, error) {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().UnixMilli()
// 时钟回拨检测
if now < s.timestamp {
return 0, errors.New("clock moved backwards")
}
if now == s.timestamp {
// 同一毫秒内,序列号+1
s.sequence = (s.sequence + 1) & maxSequence
if s.sequence == 0 {
// 序列号用完,等待下一毫秒
now = s.waitNextMillis(s.timestamp)
}
} else {
// 不同毫秒,序列号重置为0
s.sequence = 0
}
s.timestamp = now
// 组装ID
id := ((now - epoch) << timestampShift) |
(s.datacenterID << datacenterIDShift) |
(s.workerID << workerIDShift) |
s.sequence
return id, nil
}
// waitNextMillis 等待到下一毫秒
func (s *Snowflake) waitNextMillis(timestamp int64) int64 {
now := time.Now().UnixMilli()
for now <= timestamp {
now = time.Now().UnixMilli()
}
return now
}
// ParseID 解析ID
func ParseID(id int64) *IDInfo {
return &IDInfo{
Timestamp: (id >> timestampShift) + epoch,
DatacenterID: (id >> datacenterIDShift) & maxDatacenterID,
WorkerID: (id >> workerIDShift) & maxWorkerID,
Sequence: id & maxSequence,
}
}
// IDInfo ID信息
type IDInfo struct {
Timestamp int64 // 时间戳(毫秒)
DatacenterID int64 // 机房ID
WorkerID int64 // 机器ID
Sequence int64 // 序列号
}
// GetTime 获取时间
func (info *IDInfo) GetTime() time.Time {
return time.UnixMilli(info.Timestamp)
}3.2 时钟回拨处理
方案1:拒绝生成
go
func (s *Snowflake) NextIDWithReject() (int64, error) {
now := time.Now().UnixMilli()
if now < s.timestamp {
// 拒绝生成ID
return 0, fmt.Errorf("clock moved backwards, refuse to generate id")
}
// ... 正常生成
}方案2:等待时钟追上
go
func (s *Snowflake) NextIDWithWait() (int64, error) {
now := time.Now().UnixMilli()
if now < s.timestamp {
// 等待时钟追上
time.Sleep(time.Duration(s.timestamp-now) * time.Millisecond)
now = time.Now().UnixMilli()
}
// ... 正常生成
}方案3:使用备用WorkerID
go
func (s *Snowflake) NextIDWithBackup() (int64, error) {
now := time.Now().UnixMilli()
if now < s.timestamp {
// 切换到备用WorkerID
s.workerID = (s.workerID + 1) % maxWorkerID
s.sequence = 0
}
// ... 正常生成
}3.3 高性能优化
预分配ID(批量生成)
go
type IDPool struct {
snowflake *Snowflake
pool chan int64
batchSize int
}
func NewIDPool(snowflake *Snowflake, batchSize int) *IDPool {
pool := &IDPool{
snowflake: snowflake,
pool: make(chan int64, batchSize*2),
batchSize: batchSize,
}
// 后台协程持续生成ID
go pool.generate()
return pool
}
func (p *IDPool) generate() {
for {
// 批量生成ID
for i := 0; i < p.batchSize; i++ {
id, err := p.snowflake.NextID()
if err != nil {
continue
}
p.pool <- id
}
}
}
func (p *IDPool) NextID() int64 {
return <-p.pool
}3.4 Java实现(Spring Boot)
java
@Component
public class SnowflakeIdGenerator {
// 时间起点(2020-01-01)
private final long epoch = 1577808000000L;
// 机房ID占5位
private final long datacenterIdBits = 5L;
// 机器ID占5位
private final long workerIdBits = 5L;
// 序列号占12位
private final long sequenceBits = 12L;
// 最大值
private final long maxDatacenterId = ~(-1L << datacenterIdBits);
private final long maxWorkerId = ~(-1L << workerIdBits);
private final long maxSequence = ~(-1L << sequenceBits);
// 位移
private final long workerIdShift = sequenceBits;
private final long datacenterIdShift = sequenceBits + workerIdBits;
private final long timestampShift = sequenceBits + workerIdBits + datacenterIdBits;
private final long datacenterId;
private final long workerId;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(
@Value("${snowflake.datacenter-id}") long datacenterId,
@Value("${snowflake.worker-id}") long workerId) {
if (datacenterId < 0 || datacenterId > maxDatacenterId) {
throw new IllegalArgumentException("datacenterId 超出范围");
}
if (workerId < 0 || workerId > maxWorkerId) {
throw new IllegalArgumentException("workerId 超出范围");
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
/**
* 生成下一个ID
*/
public synchronized long nextId() {
long timestamp = System.currentTimeMillis();
// 时钟回拨检测
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨,拒绝生成ID");
}
if (timestamp == lastTimestamp) {
// 同一毫秒内,序列号+1
sequence = (sequence + 1) & maxSequence;
if (sequence == 0) {
// 序列号用完,等待下一毫秒
timestamp = waitNextMillis(lastTimestamp);
}
} else {
// 不同毫秒,序列号重置
sequence = 0L;
}
lastTimestamp = timestamp;
// 组装ID
return ((timestamp - epoch) << timestampShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
/**
* 等待到下一毫秒
*/
private long waitNextMillis(long lastTimestamp) {
long timestamp = System.currentTimeMillis();
while (timestamp <= lastTimestamp) {
timestamp = System.currentTimeMillis();
}
return timestamp;
}
/**
* 解析ID
*/
public IdInfo parseId(long id) {
long timestamp = ((id >> timestampShift) & ~(-1L << 41)) + epoch;
long datacenterId = (id >> datacenterIdShift) & maxDatacenterId;
long workerId = (id >> workerIdShift) & maxWorkerId;
long sequence = id & maxSequence;
return new IdInfo(timestamp, datacenterId, workerId, sequence);
}
@Data
@AllArgsConstructor
public static class IdInfo {
private long timestamp;
private long datacenterId;
private long workerId;
private long sequence;
public Date getDate() {
return new Date(timestamp);
}
}
}java
@Service
public class IdGeneratorService {
@Autowired
private SnowflakeIdGenerator snowflake;
/**
* 生成订单ID
*/
public Long generateOrderId() {
return snowflake.nextId();
}
/**
* 生成用户ID
*/
public Long generateUserId() {
return snowflake.nextId();
}
/**
* 批量生成ID
*/
public List<Long> generateBatch(int count) {
List<Long> ids = new ArrayList<>(count);
for (int i = 0; i < count; i++) {
ids.add(snowflake.nextId());
}
return ids;
}
}四、美团Leaf方案
4.1 Leaf-Segment(号段模式)
原理:
- 从数据库批量获取一个号段范围(如1-1000)
- 内存中分配ID,用完再获取下一个号段
- 双Buffer机制,提前加载下一个号段
sql
CREATE TABLE id_segment (
biz_type VARCHAR(50) PRIMARY KEY COMMENT '业务类型',
max_id BIGINT NOT NULL COMMENT '当前最大ID',
step INT NOT NULL COMMENT '步长',
description VARCHAR(255),
update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- 初始化
INSERT INTO id_segment (biz_type, max_id, step, description)
VALUES ('order', 0, 1000, '订单ID');获取号段:
sql
-- 原子操作
UPDATE id_segment
SET max_id = max_id + step
WHERE biz_type = 'order';
SELECT max_id, step FROM id_segment WHERE biz_type = 'order';
-- 返回:max_id=1000, step=1000
-- 可用范围:[1, 1000]双Buffer实现:
go
type LeafSegment struct {
segments [2]*Segment
current int // 0 or 1
mu sync.RWMutex
}
type Segment struct {
minID int64
maxID int64
step int64
current int64
}
func (l *LeafSegment) NextID() int64 {
l.mu.RLock()
segment := l.segments[l.current]
id := atomic.AddInt64(&segment.current, 1)
// 使用到10%时,异步加载下一个号段
if id > segment.minID + int64(float64(segment.step)*0.9) {
go l.loadNextSegment()
}
// 用完,切换到另一个Buffer
if id > segment.maxID {
l.mu.RUnlock()
l.switchSegment()
return l.NextID()
}
l.mu.RUnlock()
return id
}4.2 Leaf-Snowflake(改进的雪花算法)
改进点:
- WorkerID自动分配:通过Zookeeper自动分配
- 弱依赖Zookeeper:只在启动时使用
- 时钟回拨处理:容忍少量回拨
go
type LeafSnowflake struct {
*Snowflake
zk *ZookeeperClient
}
func NewLeafSnowflake(zk *ZookeeperClient) (*LeafSnowflake, error) {
// 从Zookeeper获取WorkerID
workerID, err := zk.RegisterWorker()
if err != nil {
return nil, err
}
snowflake, _ := NewSnowflake(0, workerID)
return &LeafSnowflake{
Snowflake: snowflake,
zk: zk,
}, nil
}五、性能测试
5.1 性能数据
| 方案 | QPS | 延迟P99 | 资源占用 |
|---|---|---|---|
| MySQL自增 | 1000 | 10ms | 低 |
| Redis | 10万 | 1ms | 中 |
| Snowflake | 100万 | <1ms | 低 |
| Leaf-Segment | 10万 | <1ms | 低 |
| UidGenerator | 600万 | <1ms | 中 |
5.2 压测代码
go
func BenchmarkSnowflake(b *testing.B) {
snowflake, _ := NewSnowflake(0, 0)
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
_, _ = snowflake.NextID()
}
})
}
// 结果:
// BenchmarkSnowflake-8 2000000 500 ns/op
// QPS = 1000000000 / 500 = 2,000,000六、面试要点
6.1 常见追问
Q1: 雪花算法的时钟回拨如何处理?
A: 三种方案
- 拒绝生成:直接返回错误(简单但影响可用性)
- 等待追上:sleep等待(延迟增加)
- 切换WorkerID:使用备用WorkerID(推荐)
Q2: 如何保证WorkerID的唯一性?
A:
- 配置文件:手动配置(小规模)
- Zookeeper:自动分配(中大规模,推荐)
- Redis:使用incr命令自动生成
- 数据库:从数据库表获取
Q3: Snowflake vs UUID?
A:
| 维度 | Snowflake | UUID |
|---|---|---|
| 唯一性 | 保证 | 概率保证 |
| 有序性 | 趋势递增 | 无序 |
| 长度 | 64位(8字节) | 128位(16字节) |
| 性能 | 极高 | 高 |
| MySQL索引 | 友好 | 不友好 |
Q4: 美团Leaf的优势是什么?
A:
- 高可用:双Buffer保证可用性
- 去中心化:不依赖第三方
- 性能好:批量获取,减少数据库访问
- 易扩展:支持多业务类型
6.2 扩展知识点
七、总结
分布式ID生成器选型建议:
- 小规模:MySQL自增 + 步长
- 中规模:Snowflake(推荐)
- 大规模:美团Leaf或百度UidGenerator
- 不关心顺序:UUID
雪花算法是面试最高频的方案,必须掌握:
- 64位结构:时间戳 + 机器ID + 序列号
- 时钟回拨处理
- WorkerID管理
相关场景题:
