Skip to content

如何设计分布式ID生成器

一、问题描述

1.1 业务背景

在分布式系统中,需要为订单、用户、消息等对象生成全局唯一的ID。传统的MySQL自增ID在分布式场景下无法满足需求,需要专门的分布式ID生成方案。

应用场景

  • 订单系统:生成订单号、支付单号
  • 用户系统:生成用户ID、设备ID
  • 消息系统:生成消息ID、会话ID
  • 日志系统:生成追踪ID(Trace ID)

1.2 核心需求

必须满足

  1. 全局唯一:绝对不能重复
  2. 高性能:QPS达到10万+
  3. 高可用:故障后快速恢复

最好满足: 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(改进的雪花算法)

改进点

  1. WorkerID自动分配:通过Zookeeper自动分配
  2. 弱依赖Zookeeper:只在启动时使用
  3. 时钟回拨处理:容忍少量回拨
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自增100010ms
Redis10万1ms
Snowflake100万<1ms
Leaf-Segment10万<1ms
UidGenerator600万<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: 三种方案

  1. 拒绝生成:直接返回错误(简单但影响可用性)
  2. 等待追上:sleep等待(延迟增加)
  3. 切换WorkerID:使用备用WorkerID(推荐)

Q2: 如何保证WorkerID的唯一性?

A:

  1. 配置文件:手动配置(小规模)
  2. Zookeeper:自动分配(中大规模,推荐)
  3. Redis:使用incr命令自动生成
  4. 数据库:从数据库表获取

Q3: Snowflake vs UUID?

A:

维度SnowflakeUUID
唯一性保证概率保证
有序性趋势递增无序
长度64位(8字节)128位(16字节)
性能极高
MySQL索引友好不友好

Q4: 美团Leaf的优势是什么?

A:

  1. 高可用:双Buffer保证可用性
  2. 去中心化:不依赖第三方
  3. 性能好:批量获取,减少数据库访问
  4. 易扩展:支持多业务类型

6.2 扩展知识点

七、总结

分布式ID生成器选型建议:

  1. 小规模:MySQL自增 + 步长
  2. 中规模:Snowflake(推荐)
  3. 大规模:美团Leaf或百度UidGenerator
  4. 不关心顺序:UUID

雪花算法是面试最高频的方案,必须掌握:

  • 64位结构:时间戳 + 机器ID + 序列号
  • 时钟回拨处理
  • WorkerID管理

相关场景题

正在精进