知识模块
☕ Java 知识模块
九、分布式系统
分布式 ID 生成

分布式 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 个数据中心
机器 ID5每个数据中心最多 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-snowflakeSnowflake 优化,适合消息 ID

Leaf-segment 特性

  • 双 Buffer 优化
  • 数据库高可用
  • 号段预加载

Leaf-snowflake 特性

  • Zookeeper 管理机器 ID
  • 解决时钟回拨问题

方案对比

方案性能递增依赖适用场景
UUID无需排序的场景
数据库自增数据库低并发场景
Snowflake无(需解决时钟问题)高并发场景
号段模式数据库订单 ID
RedisRedis分布式环境

面试高频问题

Q1: Snowflake 的原理?

64 位 = 1 位符号 + 41 位时间戳 + 10 位机器 ID + 12 位序列号

Q2: Snowflake 如何解决时钟回拨?

  1. 记录上次时间戳,回拨时等待或抛异常
  2. 使用 Zookeeper 管理机器 ID
  3. 美团 Leaf 方案:回拨时使用备用 workerId

Q3: 号段模式的优势?

  1. 高性能:本地生成,不依赖网络
  2. 趋势递增:利于数据库索引
  3. 高可用:双 Buffer 预加载

Q4: 为什么不用 UUID?

  1. 无序:不利于数据库索引
  2. 过长:32 个字符,存储空间大
  3. 无业务含义:无法从 ID 中提取信息

总结

分布式 ID 核心要点:
1. Snowflake:41 位时间戳 + 10 位机器 + 12 位序列
2. 号段模式:批量获取号段,本地生成
3. 推荐:美团 Leaf(开源实现)
4. 注意:时钟回拨、高可用