分布式 ID :雪花算法变体探讨

讨论分布式Id的方案,围绕雪花Id和其变体,如美团的Leaf,百度的UidGenerator等方案的一些思考

一般说起分布式id,就不得不提到雪花Id-SnowflakeId。

作为一种去中心,具有递增性质,且不会暴露真实业务量级的方案,它将64位二进制划分成1+41时间戳+10机器id+12序列号,不得不赞叹其大道至简。

一、核心痛点:时钟回拨的解决方案对比

背过八股文的我们都知道,雪花ID的最大问题就在于时钟回拨,有可能导致生成的id重复,这对于大型公司自然是不可以接受的。因此,像是美团、百度、滴滴就开源了自己的分布式id方案,本文主要讨论雪花id变体和其相关的问题,对于号段模式不做讨论。

1. 美团 Leaf-snowflake:等待追平 + 异常熔断

在美团开源的方案 Leaf-snowflake中,对于时钟问题的建议处理方案是,记录最后生成id的时间,每次生成时先进行比较,如果相差不大,那么sleep到新时间之后再生成;否则,报警并下掉该节点。下面的代码是leaf项目中的源码。

        if (timestamp < lastTimestamp) {
            long offset = lastTimestamp - timestamp;
            if (offset <= 5) {
                try {
                    wait(offset << 1);
                    timestamp = timeGen();
                    if (timestamp < lastTimestamp) {
                        return new Result(-1, Status.EXCEPTION);
                    }
                } catch (InterruptedException e) {
                    LOGGER.error("wait interrupted");
                    return new Result(-2, Status.EXCEPTION);
                }
            } else {
                return new Result(-3, Status.EXCEPTION);
            }
        }
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            if (sequence == 0) {
                //seq 为0的时候表示是下一毫秒时间开始对seq做随机
                sequence = RANDOM.nextInt(100);
                timestamp = tilNextMillis(lastTimestamp);
            }
        } else {
            //如果是新的ms开始
            sequence = RANDOM.nextInt(100);
        }

其实,除了等待追平之外,还可以优先复用lastTimestamp中未生成完的序列id。这样当发生时钟回拨时,先判断上次的序列是否达到上限,没有的话依然使用上次的时间戳继续生成。直到达到上限后再去进行追平或报警。可能差别不是很大,但我个人倾向于可以先用再追平。

2.百度 UidGenerator:两种模式适配不同场景

在百度的方案UidGenerator中,

DefaultUidGenerator的思路是等待时间追平后再生成id,和美团类似。

CachedUidGenerator则是通过预支未来的时间,其核心是启动时取当前时间的秒级时间戳,之后通过long的cas自增操作进行+1(不依赖系统的时间)–类比单机从1开始,然后做自增操作。不过这里的起始值是具有时间属性的。而为了存储预支的时间,设计了两个buffer来维护。一个用于存储id,一个用于判断是否可以存储Id。

CachedUidGenerator 将「时间依赖」转移到启动阶段,运行时完全脱离系统时间,既保证了趋势递增,又从根源上解决了时钟回拨导致的 ID 重复问题。

二、关键细节:WorkerId 的分配与复用

百度的UIdGenerator为了保证workId的自增和统一管理,默认是利用db来进行存储(当然也可以通过其他的方式,实现WorkerIdAssigner接口),使用自增的id主键来作为workId,目前是不会重复使用workId,因为没有在外部记录预支出的最大的时间,如果使用原来的workId,那由于不知道预支出到何时,因此无法投入使用。不过这点,我感觉在表中新增一个字段用来存储已经分配的最大时间,那么在等到该时间追平后好像就可以使用了?

而美团的则通过zk注册顺序节点来保证,并且考虑到了在zk节点中存储该节点的最后生成时间,这样可以在节点重启时继续使用而不用分配新的workId。且为了考虑效率,生成时间只在 启动的时候进行校验,内部真实生成id的时候还是比较的内存,而不是从zk现读取。

三、并发优化:多线程安全与性能提升

1. 美团 Leaf:synchronized 关键字保证原子性

Leaf 直接使用synchronized修饰生成 ID 的方法,确保同一时间只有一个线程操作timestampsequence

public synchronized Result get(String key) {
    // ID生成逻辑...
}

2. 百度 UidGenerator:PaddedAtomicLong 避免伪共享

UidGenerator 使用自定义的PaddedAtomicLong替代原生AtomicLong,核心解决 CPU 伪共享问题:

public class PaddedAtomicLong extends AtomicLong {
    private static final long serialVersionUID = -3415778863941386253L;
    // CPU缓存行通常为64字节,通过填充6long48字节)避免伪共享
    public volatile long p1, p2, p3, p4, p5, p6 = 7L;
}

伪共享问题:多个原子变量若处于同一 CPU 缓存行,一个变量的修改会导致整个缓存行失效,频繁触发缓存同步,降低性能;

优化逻辑:通过填充 6 个 long 变量(共 48 字节),让PaddedAtomicLong的核心值独占一个 64 字节缓存行,避免缓存失效带来的性能损耗。

优势:无锁设计,CAS 操作保证原子性,支持每秒百万级 ID 生成,适合超高并发场景。

四、架构选型:中心化与去中心化的取舍

对于雪花id的组成部分时间戳+workId+序列化三部分而言,其中workId部分如果完成由人工配置,那就是去中心化的,但是如果依赖于zk或者db等方式分配,那么该方案就有一定的中心化因素。且美团的leaf为了监控每个节点的状态,以及节点的上下线,整体依赖zk要更多。不过leaf和UIdGenerator尽管依赖的核心挂掉,那么在已经生成了workId的节点就不会受到影响。

五、网络与部署

由于id本身的生成非常快,那么耗时大部分就是网络耗时。不过这个各个语言都有较成熟的代码了,如果是java,可以直接使用netty在自定义请求中,返回id。如果是c语言,那么可以复用redis或者nginx的网络模型部分,在里面进行处理。

而调用方可以简单的遍历节点列表,加简单的重试机制。或者外包给slb或者nginx,调用方直接调用nginx代理的域名去做处理。

参考:

Leaf——美团点评分布式ID生成系统 - 美团技术团队

https://github.com/Meituan-Dianping/Leaf

https://github.com/baidu/uid-generator

Licensed under CC BY-NC-SA 4.0
最后更新于 2025-11-26