Skip to content
第 13 章 架构 ⏱ 10 分钟阅读

第 13 章:分布式 ID ​

学习目标 ​

  • 理解分布式 ID 的核心要求:全局唯一、趋势递增
  • 掌握雪花算法(Snowflake)的实现
  • 学会号段模式和 Leaf 方案
  • 避免时间回拨、ID 重复的坑

一、为什么不用数据库自增? ​

text
单库时代
[订单表] → id = 1, 2, 3, 4...    ✅ 没问题

分库分表后
[订单库 0] → id = 1, 3, 5, 7...  (奇数)
[订单库 1] → id = 2, 4, 6, 8...  (偶数)
每个库自增不同 = 重复 ❌

分布式 ID 三大要求:

  1. 全局唯一:跨库、跨表、跨服务
  2. 高性能:每秒生成数万 ID
  3. 趋势递增:主键排序、MySQL 索引(B+ 树追加,避免页分裂)

二、UUID ​

java
// 简单方案
String id = UUID.randomUUID().toString();
// 550e8400-e29b-41d4-a716-446655440000
text
优点:本地生成,无依赖
缺点:无序(36 字符串,占空间)、不可读、不递增
java
// 数据库表设计
CREATE TABLE order (
    id BINARY(16) PRIMARY KEY,        -- UUID 二进制存
    -- ...
);

⚠️ 坑 1:UUID 当主键,MySQL InnoDB 主键聚簇索引,UUID 无序 → 频繁页分裂,插入性能差。

三、雪花算法(Snowflake) ​

Twitter 2010 年提出,64 bit 整数:

text
| 1 bit 符号 | 41 bit 时间戳 | 10 bit 机器 | 12 bit 序列 |
|     0     |  当前毫秒     |  1024 节点  |  4096/ms   |
java
public class SnowflakeIdGenerator {
    private static final long EPOCH = 1700000000000L;  // 起始时间戳
    private static final long MACHINE_BITS = 10;
    private static final long SEQUENCE_BITS = 12;

    private final long machineId;
    private long lastTimestamp = -1L;
    private long sequence = 0;
    private final Object lock = new Object();

    public SnowflakeIdGenerator(long machineId) {
        if (machineId < 0 || machineId >= (1 << MACHINE_BITS)) {
            throw new IllegalArgumentException("机器 ID 超出范围");
        }
        this.machineId = machineId;
    }

    public long nextId() {
        synchronized (lock) {
            long timestamp = System.currentTimeMillis();

            if (timestamp < lastTimestamp) {
                throw new RuntimeException("时钟回拨,等待" + (lastTimestamp - timestamp) + "ms");
            }

            if (timestamp == lastTimestamp) {
                sequence = (sequence + 1) & ((1 << SEQUENCE_BITS) - 1);
                if (sequence == 0) {
                    // 同一毫秒序列号满,等下一毫秒
                    timestamp = waitNextMillis(lastTimestamp);
                }
            } else {
                sequence = 0;
            }

            lastTimestamp = timestamp;

            return ((timestamp - EPOCH) << (MACHINE_BITS + SEQUENCE_BITS))
                 | (machineId << SEQUENCE_BITS)
                 | sequence;
        }
    }

    private long waitNextMillis(long last) {
        long ts = System.currentTimeMillis();
        while (ts <= last) {
            ts = System.currentTimeMillis();
        }
        return ts;
    }
}
java
// 使用
SnowflakeIdGenerator generator = new SnowflakeIdGenerator(1);  // 机器 ID = 1
for (int i = 0; i < 10; i++) {
    System.out.println(generator.nextId());
}
// 734981723456000001 ...

⚠️ 坑 2:时钟回拨(系统时间被改到过去)雪花会抛异常。生产机器禁 NTP 自动同步,或代码里等回拨结束。

四、美团 Leaf ​

java
// 号段模式:每次从 DB 取一段 ID
public class LeafSegment {
    private final AtomicLong currentId;
    private final long maxId;
    private final String key;
    private final SegmentDao dao;

    public LeafSegment(String key, SegmentDao dao) {
        this.key = key;
        this.dao = dao;
        Segment s = dao.loadSegment(key);
        this.currentId = new AtomicLong(s.getMinId());
        this.maxId = s.getMaxId();
    }

    public long nextId() {
        long id = currentId.incrementAndGet();
        if (id > maxId) {
            // 异步加载下一段
            synchronized (this) {
                if (currentId.get() > maxId) {
                    Segment next = dao.loadNextSegment(key);
                    currentId.set(next.getMinId());
                    // ...
                }
            }
        }
        return id;
    }
}
sql
-- 数据库表
CREATE TABLE leaf_alloc (
    biz_tag VARCHAR(50) PRIMARY KEY,
    max_id BIGINT NOT NULL,
    step INT NOT NULL,           -- 步长,默认 1000
    update_time TIMESTAMP NOT NULL
);

五、百度 UidGenerator ​

java
// 基于 Snowflake 改进
// 1. 用未来时间解决时钟回拨
// 2. 借助 DB 分配 workId
// 3. RingBuffer 预生成 ID
java
@Service
public class UidGeneratorService {
    @Resource
    private DefaultUidGenerator uidGenerator;

    public long nextId() {
        return uidGenerator.getUID();
    }

    public String nextIdStr() {
        return uidGenerator.getUIDStr();
    }
}

六、ID 性能与方案选择 ​

方案TPS趋势递增长度场景
UUID极高无32临时 ID
DB 自增1000✅8单库
雪花10万+✅8通用
Leaf 号段10万+✅8高并发
Leaf 雪花10万+✅8长期

七、ID 编码可读性 ​

java
// 业务前缀:ORD + 雪花 ID
public class OrderId {
    private final long value;

    public OrderId(long value) {
        this.value = value;
    }

    public String encoded() {
        return "ORD" + value;
    }

    public String toString() {
        return encoded();
    }
}

// 用户前缀:USER
public class UserId {
    public String encoded() {
        return "USER" + value;
    }
}

⚠️ 坑 3:日志里看到 id=789123456789012345,不知道是订单还是用户。前缀化让日志一眼可读。

八、ID 发送的可靠性 ​

java
// Worker 模式:批量预生成,异步填充
public class IdWorker {
    private final BlockingQueue<Long> queue = new LinkedBlockingQueue<>(10000);

    @PostConstruct
    public void init() {
        new Thread(() -> {
            while (true) {
                if (queue.size() < 1000) {
                    for (int i = 0; i < 1000; i++) {
                        queue.offer(snowflake.nextId());
                    }
                }
                Thread.sleep(10);
            }
        }).start();
    }

    public long nextId() {
        return queue.poll();
    }
}

本章小结 ​

方案特点
UUID简单,无序,占空间
Snowflake高性能,趋势递增,时钟敏感
Leaf 号段稳定,DB 持久化
Leaf 雪花雪花的优化
关键点建议
业务前缀ORD/USER/PAY
长度64 bit,Long 类型
时钟禁 NTP 自动同步
重复检测唯一索引兜底

动手练习 ​

  1. 雪花实现:写一个 SnowflakeIdGenerator,生成 100 万 ID 验证唯一
  2. Leaf 接入:用美团 Leaf 在 Spring Boot 中集成号段模式
  3. 时钟回拨:模拟时钟回拨,看雪花是否抛异常
  4. 业务前缀:给订单 ID 加 ORD 前缀,验证可读性

下一章:第 14 章:分布式事务 →

本站基于 VitePress 构建 · 由 StackHub 团队维护