全局唯一 ID 生成与雪花算法

全局唯一 ID 生成与雪花算法

分布式环境的 ID 需求

分库分表后,数据库自增主键不再可用——两个分片从 1 开始自增,会导致不同分片出现相同 ID。分布式系统需要一个全局唯一的 ID 生成方案。

全局 ID 的要求

  • 全局唯一:绝对不能重复
  • 趋势递增:便于数据库 B+Tree 索引维护,减少页分裂
  • 高性能:生成速度快(微秒级),不能成为系统瓶颈
  • 高可用:服务不能单点,出故障时仍能正常生成
  • 有序性:排序、分页等操作依赖 ID 的有序性

常见方案对比

方案 特点 问题
UUID 简单,全球唯一 不递增,太长(36 字符),碎片化严重
数据库自增 有 ID 表或号段模式 性能一般,依赖于 DB
Redis 自增 INCR 命令,高性能 需要维护 Redis 集群
雪花算法 趋势递增,高性能 依赖时钟(NTP 对时问题)

雪花算法(Snowflake)

算法原理

Twitter 开源的分布式 ID 生成算法,结果是 64 位 long 整数。

bit 位结构(共 64 位):
 0 | 0000000000 0000000000 0000000000 0000000000 00 | 00000 | 00000 | 000000000000

各段含义:
第 1 位:符号位(始终为 0,保证 ID 为正数)
41 位:时间戳(毫秒级,可用 69 年)
10 位:工作机器 ID(最多 1024 台机器)
12 位:序列号(每毫秒可生成 4096 个 ID)

细分:
 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000
 符号     时间戳(毫秒,相对于2020-01-01)     数据中心ID  机器ID   序列号

ID 生成示例

// 简化版雪花算法实现
public class SnowflakeIdWorker {
    // 开始时间戳(2020-01-01)
    private final long twepoch = 1577836800000L;

    // 机器 ID 所占位数
    private final long workerIdBits = 10L;
    // 序列号所占位数
    private final long sequenceBits = 12L;

    // 最大值
    private final long maxWorkerId = ~(-1L << workerIdBits); // 1023
    private final long sequenceMask = ~(-1L << sequenceBits); // 4095

    // 偏移量
    private final long workerIdShift = sequenceBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits;

    private long workerId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public synchronized long nextId() {
        long timestamp = timeGen();

        // 时钟回拨处理
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format("Clock moved backwards. Refusing for %d ms", 
                    lastTimestamp - timestamp));
        }

        if (lastTimestamp == timestamp) {
            // 同一毫秒内,序列号递增
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) { // 当前毫秒用完 4096 个 ID
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L; // 新毫秒,序列号重置
        }

        lastTimestamp = timestamp;

        // 组合为 64 位 ID
        return ((timestamp - twepoch) << timestampLeftShift)
             | (workerId << workerIdShift)
             | sequence;
    }
}

雪花算法的优势

  1. 趋势递增:按时间顺序生成的 ID 在 B+Tree 中相邻插入,减少页分裂
  2. 高性能:纯内存计算,每毫秒单机支持 409.6 万个 ID
  3. 简单可靠:不需要网络通信,本地生成
  4. 反解信息:可以从 ID 中解析出生成时间和机器 ID

雪花算法的问题

1. 时钟回拨

这是最严重的问题。如果系统时间回拨(NTP 调整或人为修改),可能生成重复 ID:

// 处理方式
if (timestamp < lastTimestamp) {
    // 方案一:直接抛出异常(使用中不推荐)
    // 方案二:等待时间追上来
    // 方案三:记录回拨,用备用序列号生成
    waitUntil(lastTimestamp); // 等待到上次时间
}

2. 工作节点 ID 管理

每台机器的 workerId 需要手动或通过协调服务分配(如 ZooKeeper)。

改进版:百度的 UidGenerator 和美团的 Leaf

美团 Leaf

基于雪花算法 + 号段模式,解决了时钟回拨问题:

Leaf-segment:使用数据库号段模式,从 DB 中取号段
Leaf-snowflake:雪花算法的改进,用 ZooKeeper 管理 workerId
              时钟回拨时等待,超过阈值则告警但不阻塞
// Leaf 处理时钟回拨:
// 10ms 以内:等待
// 10ms 以上:告警并上报
// 等待过程中不生成 ID

百度 UidGenerator

通过自定义时间戳起始值和 workerId 分配策略,支持更灵活的配置。

字节跳动 Leaf (Tinyid)

中心化的 ID 生成服务,客户端批量取号段。

面试要点

  • 雪花算法是分布式 ID 的首选方案(性能好、趋势递增)
  • 核心缺陷是时钟回拨——需要理解原因和常见处理策略
  • 机械硬盘时代 UUID 是灾难(随机写入),SSD 时代有所缓解但仍不推荐
  • 面试中能画出雪花算法的 64 位结构图
  • 了解主流改进版(Leaf、UidGenerator)的核心设计思路
© 版权声明
THE END
喜欢就支持一下吧
点赞7 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容