💎 还在用 UUID?90% 的大厂都在用这套分布式 ID 生成方案(深度长文)

75 阅读6分钟

在分布式系统中,生成全局唯一 ID 是一个看似简单、实则深坑无数的基础问题。

“不就是生成个 ID 吗?用 UUID.randomUUID() 不就行了?”
如果你在面试或架构评审中这么说,大概率会被“请”出去。

为什么?UUID 无序、太长、会导致数据库页分裂……
那么,数据库自增 ID 呢?分库分表后怎么办?
雪花算法(Snowflake)?时钟回拨问题怎么解?

今天,我们不讲虚的,用万字长文(夸张修辞,实则干货满满)带你彻底打通分布式 ID 生成的任督二脉。从原理到源码,从方案选型到生产级落地,一篇管饱!


🎯 核心需求:我们需要什么样的 ID?

一个优秀的分布式 ID,必须满足以下  “四大金刚”

  1. 全局唯一:这是最基本的,不能重复。
  2. 趋势递增:这对数据库索引(尤其是 MySQL InnoDB)非常重要,能减少页分裂,提升写入性能。
  3. 高性能:生成 ID 的动作不能成为系统的瓶颈,延迟要低,吞吐要高。
  4. 高可用:ID 生成服务挂了,整个系统就瘫痪了,所以必须 5 个 9 (99.999%) 可用。

加分项

  • 信息安全:ID 最好不要直接暴露业务量(比如不要连续自增,否则竞品一下就知道你每天有多少订单)。
  • 短小精悍:存储空间越小越好。

⚔️ 方案演进之路

1. ❌ 方案一:UUID (Java 自带)

String id = UUID.randomUUID().toString().replace("-", "");
// 结果:c2b8c2b8c2b8c2b8c2b8c2b8c2b8c2b8
  • 优点:本地生成,性能无敌,没有网络消耗。

  • 缺点

    • 太长:32 位 16 进制字符串,占用空间大。
    • 无序:完全随机,导致 MySQL B+ 树索引频繁分裂,写入性能极差(这是致命伤)。
    • 不可读:无法从 ID 中获取时间、业务等信息。
  • 结论坚决不用做主键,只适合做 Token 或文件名。

2. ⚠️ 方案二:数据库自增 ID (MySQL)

利用 MySQL 的 auto_increment 特性。

  • 优点:简单,单调递增,查询快。

  • 缺点

    • 单点故障:数据库挂了就完了。
    • 性能瓶颈:单机数据库写入能力有限。
    • 分库分表麻烦:不同库生成的 ID 会重复。
  • 改进版:号段模式 (Leaf-segment)

    • 原理:不每次都去数据库取 ID,而是去取一个“号段”(比如一次取 1000 个)。业务服务在内存里慢慢发,发完了再去取下一个号段。
    • 优点:大大减轻数据库压力。
    • 缺点:ID 是连续的,容易暴露业务量;服务重启会浪费 ID。

3. ✅ 方案三:雪花算法 (Snowflake) —— 业界标杆

这是 Twitter 开源的算法,也是目前最主流的方案。它生成的是一个 64 位的 Long 型整数

结构拆解 (64 bit)

  • 1 bit:不使用 (符号位)。
  • 41 bits:毫秒级时间戳 (可以使用 69 年)。
  • 10 bits:机器 ID (5位数据中心 ID + 5位工作机器 ID,支持 1024 个节点)。
  • 12 bits:毫秒内的序列号 (每毫秒产生 4096 个 ID)。

理论吞吐量:4096 * 1000 = 400 万 ID/秒


🛠️ 手写一个生产级 Snowflake (解决时钟回拨)

雪花算法最大的坑是  “时钟回拨” 。如果服务器时间回调了,可能会生成重复 ID。

下面的代码是一个改良版,增加了时钟回拨的检测和处理。

/**
 * 生产级雪花算法实现
 */
public class SnowflakeIdGenerator {

    // 起始时间戳 (例如:2024-01-01 00:00:00)
    private final long twepoch = 1704067200000L;

    // 各部分占用的位数
    private final long workerIdBits = 5L;
    private final long datacenterIdBits = 5L;
    private final long sequenceBits = 12L;

    // 各部分最大值 (通过位移计算)
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits); // 31
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits); // 31
    private final long sequenceMask = -1L ^ (-1L << sequenceBits); // 4095

    // 各部分左移位数
    private final long workerIdShift = sequenceBits;
    private final long datacenterIdShift = sequenceBits + workerIdBits;
    private final long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;

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

    public SnowflakeIdGenerator(long workerId, long datacenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (datacenterId > maxDatacenterId || datacenterId < 0) {
            throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId));
        }
        this.workerId = workerId;
        this.datacenterId = datacenterId;
    }

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

        // 🚨 核心逻辑:时钟回拨处理
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                // 如果回拨时间很短(<=5ms),等待追上来
                try {
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
                    }
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            } else {
                // 回拨过大,直接报错,或者切换 workerId (生产环境建议报警)
                throw new RuntimeException(String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
            }
        }

        // 如果是同一毫秒生成的
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            // 毫秒内序列溢出 (超过4095)
            if (sequence == 0) {
                // 阻塞到下一毫秒
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            // 新的一毫秒,序列号重置
            sequence = 0L;
        }

        lastTimestamp = timestamp;

        // 拼接 ID
        return ((timestamp - twepoch) << timestampLeftShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;
    }

    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    private long timeGen() {
        return System.currentTimeMillis();
    }
}

🏢 大厂开源方案剖析

如果不想自己造轮子,可以直接用大厂的开源组件,它们解决了很多边缘问题。

1. 美团 Leaf

Leaf 提供了两种模式:

  • Leaf-segment (号段模式) :基于 MySQL,性能高,但 ID 递增。
  • Leaf-snowflake (雪花模式) :基于 ZooKeeper 持久化节点来分配 WorkerID,解决了 WorkerID 难以管理的问题。同时对时钟回拨做了非常完善的校验。

2. 百度 UidGenerator

  • 特点:基于 Snowflake,但打破了传统位数分配。
  • RingBuffer 缓存:它使用 RingBuffer 缓存生成的 ID,吞吐量极高(压测可达 600万 QPS)。
  • 缺点:依赖 MySQL,配置稍显复杂。

💡 架构师的最终建议

在做技术选型时,不要盲目追求“最强”,要追求“最合适”。

  1. 中小项目 / 单体应用

    • 直接用 MyBatis-Plus 自带的 IdWorker (基于 Snowflake)。开箱即用,不用写一行代码。
    • 配置:@TableId(type = IdType.ASSIGN_ID)。
  2. 分库分表 / 微服务初期

    • 使用上面的 手写 Snowflake 代码,或者引入 Hutool 工具包 的 IdUtil.getSnowflake()。
    • 注意:要保证每个服务的 workerId 和 datacenterId 不重复(可以通过 Redis 或环境变量动态分配)。
  3. 超高并发 / 核心金融系统

    • 上 美团 Leaf。它经过了美团内部万亿级流量的考验,稳定性极强,且有完善的监控和容灾机制。

📝 总结

方案全局唯一趋势递增性能复杂度适用场景
UUID⭐⭐⭐⭐⭐临时 Token、文件名
MySQL 自增⭐⭐小规模单体应用
Snowflake⭐⭐⭐⭐⭐⭐绝大多数互联网业务
美团 Leaf⭐⭐⭐⭐⭐⭐⭐⭐大厂核心业务、高并发

希望这篇深度长文能帮你彻底搞定分布式 ID!下次面试官问你,直接把这篇文章的逻辑甩给他!😎