分布式ID模式:从UUID到美团Leaf的实战之路

分布式ID,说白了就是在分布式系统里生成全局唯一的ID。你想想看,单机时代我们用数据库自增主键就搞定了,但到了微服务架构下,多个服务同时写数据,怎么保证ID不冲突?这就是分布式ID要解决的问题。

我这些年做过不少分布式系统,从最早用UUID到后来用雪花算法,再到研究美团Leaf方案,踩过不少坑。今天就把这些经验分享给你。

为什么需要分布式ID?

先说说业务场景。订单系统、用户系统、消息系统,这些场景都需要全局唯一的ID。而且要求还不少:

  • 全局唯一性:这是最基本的要求,不能重复
  • 趋势递增:数据库B+树索引对有序插入更友好
  • 高可用:ID生成服务不能挂,挂了整个系统就瘫了
  • 高性能:生成速度要快,不能成为瓶颈

嗯,这里要注意,不同场景对ID的要求不一样。比如日志系统可能只要求唯一,但订单系统还要求趋势递增。

常见方案对比

我整理了一张对比表,方便你快速了解各种方案的优劣:

方案 优点 缺点 适用场景
UUID 实现简单,本地生成,无网络开销 太长(36位),无序,影响索引性能 日志、文件命名等不要求有序的场景
数据库自增 实现简单,有序递增 单点瓶颈,扩展性差 低并发、小规模系统
Redis生成 性能高,有序递增 依赖Redis,网络开销 中等并发,可接受Redis依赖
雪花算法 高性能,趋势递增,不依赖外部服务 依赖机器时钟,时钟回拨会出问题 大部分分布式系统
美团Leaf 高可用,高性能,解决时钟回拨 实现复杂,需要额外组件 大型分布式系统

UUID方案:简单但别滥用

UUID是最简单的方案。Java里直接UUID.randomUUID()就搞定了。但我个人习惯是,除非场景特别简单,否则别用UUID做主键。

为什么?UUID是128位的,通常表示为36位字符串。无序插入会导致B+树频繁分裂,性能下降明显。我在项目中遇到过,一张千万级的表用UUID做主键,插入速度比自增ID慢了5倍以上。

// Java示例:生成UUID
String id = UUID.randomUUID().toString();
// 输出类似:550e8400-e29b-41d4-a716-446655440000

// 去掉横线,缩短长度
String shortId = id.replaceAll("-", "");
// 输出:550e8400e29b41d4a716446655440000
避坑指南:我曾经在一个日志系统里用了UUID,结果日志量大了之后,ES索引性能直线下降。后来改成雪花算法,问题才解决。所以,UUID适合做唯一标识,但不适合做数据库主键。

数据库自增:老派但可靠

数据库自增ID是最传统的方案。利用数据库的AUTO_INCREMENT特性,每次插入时自动生成递增ID。

-- MySQL示例
CREATE TABLE id_generator (
    id BIGINT NOT NULL AUTO_INCREMENT,
    stub CHAR(1) NOT NULL DEFAULT '',
    PRIMARY KEY (id),
    UNIQUE KEY stub (stub)
);

-- 获取ID
REPLACE INTO id_generator (stub) VALUES ('a');
SELECT LAST_INSERT_ID();

这个方案的优点是简单可靠,但缺点也很明显:单点瓶颈。高并发下数据库会成为瓶颈。而且如果数据库挂了,整个系统都无法生成ID。

我记得有个项目初期用这个方案,后来并发量上来了,数据库压力太大,不得不迁移到雪花算法。

Redis生成:高性能但依赖外部

Redis的INCR命令可以原子性地递增一个值,非常适合生成分布式ID。

// Java示例:使用Redis生成ID
StringRedisTemplate redisTemplate;
Long id = redisTemplate.opsForValue().increment("order_id_key", 1);

Redis方案性能很高,单机QPS可以达到10万+。但问题是依赖Redis,如果Redis挂了,ID生成就停了。而且Redis的持久化机制可能导致ID重复(比如RDB快照丢失了部分数据)。

我的建议:如果要用Redis方案,一定要开启AOF持久化,并且配置appendfsync always。虽然性能会下降一些,但能保证ID不重复。

雪花算法:目前最流行的方案

雪花算法是Twitter开源的分布式ID生成算法。它生成的ID是一个64位的Long型整数,结构如下:

| 1位符号位 | 41位时间戳 | 10位工作机器ID | 12位序列号 |
  • 1位符号位:固定为0,表示正数
  • 41位时间戳:毫秒级时间戳,可以使用69年
  • 10位工作机器ID:支持1024台机器
  • 12位序列号:同一毫秒内支持4096个ID
// Java示例:雪花算法核心实现
public class SnowflakeIdWorker {
    private final long workerId;
    private final long datacenterId;
    private long sequence = 0L;
    private long lastTimestamp = -1L;

    public synchronized long nextId() {
        long timestamp = timeGen();
        
        // 时钟回拨处理
        if (timestamp < lastTimestamp) {
            throw new RuntimeException("时钟回拨");
        }
        
        if (timestamp == lastTimestamp) {
            sequence = (sequence + 1) & 4095;
            if (sequence == 0) {
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            sequence = 0L;
        }
        
        lastTimestamp = timestamp;
        
        return ((timestamp - 1288834974657L) << 22)
                | (datacenterId << 17)
                | (workerId << 12)
                | sequence;
    }
}
核心要点:雪花算法的关键在于时钟回拨问题。如果机器时钟回拨了,生成的ID可能重复。我建议在代码里加一个容忍阈值,比如回拨在5毫秒以内就等待,超过就抛异常。

美团Leaf方案:企业级解决方案

美团Leaf是美团开源的分布式ID生成方案,它解决了雪花算法的时钟回拨问题,同时提供了更高的可用性。

Leaf有两种模式:

  • Leaf-segment:基于数据库的号段模式,预取一批ID到内存中
  • Leaf-snowflake:改进的雪花算法,用ZooKeeper管理机器ID

我个人比较推荐Leaf-snowflake模式。它用ZooKeeper来分配工作机器ID,避免了手动配置的麻烦。而且它解决了时钟回拨问题:如果回拨时间小于5毫秒,就等待;如果大于5毫秒,就报错。

// Leaf-snowflake核心逻辑(简化版)
public class LeafSnowflakeIdWorker {
    private final long workerId;
    private final long twepoch = 1288834974657L;
    private long sequence = 0L;
    private long lastTimestamp = -1L;
    private static final long CLOCK_BACK_THRESHOLD = 5; // 5毫秒阈值

    public synchronized long nextId() {
        long timestamp = timeGen();
        
        // 时钟回拨处理
        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= CLOCK_BACK_THRESHOLD) {
                // 等待
                Thread.sleep(offset);
                timestamp = timeGen();
            } else {
                throw new RuntimeException("时钟回拨超过阈值");
            }
        }
        
        // 后续逻辑同雪花算法
        // ...
    }
}

如何选择?

说了这么多,到底该用哪个?我根据经验给你一些建议:

  • 小项目、低并发:数据库自增就够用了,别折腾
  • 中等规模、对有序性要求不高:UUID也行,但别做主键
  • 大部分分布式系统:雪花算法,注意处理好时钟回拨
  • 大型系统、高可用要求:美团Leaf,或者自己实现类似方案
我的经验:如果你用的是Spring Cloud微服务架构,可以考虑用雪花算法+Redis做备用。主用雪花算法,如果时钟回拨了,就切换到Redis生成。这样既保证了性能,又解决了时钟回拨问题。

知识体系图

下面这张图展示了分布式ID方案的整体知识结构:

分布式ID方案知识体系 分布式ID UUID 128位,36字符 无序,索引性能差 数据库自增 单点瓶颈 有序递增 Redis生成 高性能,依赖Redis 持久化可能丢数据 雪花算法 64位Long型 时钟回拨问题 美团Leaf Leaf-segment(号段) Leaf-snowflake(改进)

这张图把五种方案的关系和特点都展示出来了。你可以看到,从简单到复杂,从单机到分布式,每种方案都有它的适用场景。

总结

分布式ID没有银弹。每种方案都有优缺点,关键是根据你的业务场景来选择。我个人建议:

  • 如果项目刚起步,用数据库自增或雪花算法就够了
  • 如果并发量上来了,考虑用美团Leaf或者自己实现类似方案
  • 不管用哪种方案,一定要做好监控和容错

嗯,今天就聊到这里。分布式ID看起来简单,但真正做好并不容易。希望这些经验能帮你少走一些弯路。


公众号:蓝海资料掘金营,微信deep3321