调研百度 UID-Generator

259 阅读3分钟

原文首发于我的博客:kaolengmian7.com/posts/baidu… PC 端访问我的博客可以获得最优质的阅读体验同时也可以翻阅我的其他博文。


百度 UID-Generator 使用 Java 语言,基于雪花算法实现。

UID-Generator 生成的 Id 格式:

image.png

  • sign
    固定 1 bit 符号标识,即生成的 UID 为正数。
  • delta seconds
    当前时间,相对于时间基点"2016-05-20"**(可以自行配置)**的增量值,单位:,“最多”可支持约 8.7 年
  • workerId
    机器 id,最多可支持约 420w 次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
  • sequence
    每秒下的并发序列,13 bits可支持每秒 8192 个并发。

以上参数均可通过Spring配置文件进行自定义,比如:

<!-- DefaultUidGenerator -->
<bean id="defaultUidGenerator" class="com.baidu.fsg.uid.impl.DefaultUidGenerator" lazy-init="false">
    <property name="workerIdAssigner" ref="disposableWorkerIdAssigner"/>

    <!-- Specified bits & epoch as your demand. No specified the default value will be used -->
    <property name="timeBits" value="29"/>
    <property name="workerBits" value="21"/>
    <property name="seqBits" value="13"/>
    <property name="epochStr" value="2016-09-20"/>
</bean>
 
<!-- 用完即弃的WorkerIdAssigner,依赖DB操作 -->
<bean id="disposableWorkerIdAssigner" class="com.baidu.fsg.uid.worker.DisposableWorkerIdAssigner" />

UID-Generator 的特点

1. 依赖 Mysql 做 WorkerId 分发,使用 Mysql 自增 Id 做 workerId,用后即弃。

workerId 默认 22 位,这意味着集成 UidGenerator 生成分布式ID的所有实例重启次数是不允许超过4194303次(即2^22-1),否则会抛出异常。

2. 以秒为单位(区别传统雪花算法)。 3. 支持自定义位数。 4. 本地化:UidGenerator 以组件形式工作在应用项目中。

UID-Generator 的实现

UID-Generator 有两种实现,一种是基于传统雪花算法实现(DefaultUidGenerator),另一种是百度基于雪环算法做的优化版本(CachedUidGenerator)。

DefaultUidGenerator

DefaultUidGenerator 依照雪花算法实现,它采用抛异常的方式来处理雪花算法的“时间回拨”问题。

protected synchronized long nextId() {
    long currentSecond = getCurrentSecond();

    // 时间回拨 -> 抛异常
    if (currentSecond < lastSecond) {
        long refusedSeconds = lastSecond - currentSecond;
        throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
    }

    // 在同一秒内,sequence 自增
    if (currentSecond == lastSecond) {
        sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
        // Exceed the max sequence, we wait the next second to generate uid
        if (sequence == 0) {
            currentSecond = getNextSecond(lastSecond);
        }
    // 不在同一秒:重置 sequence
    } else {
        sequence = 0L;
    }

    lastSecond = currentSecond;

    // 分配 Id
    return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}

CachedUidGenerator

根据上面的实现,我们知道 sequence 决定了 UID-Generator 的并发能力,13 bits 的 sequence 可支持 8192/s 的并发,现实中很有可能不够用,进而诞生了 CachedUidGenerator。
CachedUidGenerator 使用 RingBuffer 缓存生成的id。RingBuffer是个环形数组,默认大小为 8192 个(可以通过boostPower参数设置大小)。

RingBuffer 的运行原理:

RingBuffer环形数组,数组每个元素成为一个 slot。
Tail 指针、Cursor 指针用于环形数组上读写 slot:

  • Tail 指针
    表示 Producer 生产的最大序号(此序号从 0 开始,持续递增)。Tail 不能超过 Cursor,即生产者不能覆盖未消费的 slot。当 Tail 已赶上 curosr,此时可通过 rejectedPutBufferHandler 指定 PutRejectPolicy。
  • Cursor 指针
    表示 Consumer 消费到的最小序号(序号序列与 Producer 序列相同)。Cursor 不能超过 Tail,即不能消费未生产的 slot。当 Cursor 已赶上 tail,此时可通过 rejectedTakeBufferHandler 指定 TakeRejectPolicy。

image.png
RingBuffer 填充触发机制:

  • 程序启动时,将 RingBuffer 填充满。
  • 在调用 getUID() 获取 id 时,检测到 RingBuffer 中的剩余 id 个数小于总个数的 50%,将 RingBuffer 填充满。
  • 定时填充(可配置是否使用以及定时任务的周期)。

为什么 CachedUidGenerator 性能强?

根据上面的研究我们得知:即使在同一秒内,只要触发填充条件,RingBuffer 就会立即填充。如果在 1s 内多次填充 RingBuffer,代码会自动增加 delta seconds,就像“借用”了未来的时间一样。
得益于此 UID-Generator 也突破了 8192 QPS,可以拿到 600w+ QPS 的好成绩。

总结

百度 UID-Generator 性能强劲,尤其是 CachedUidGenerator 实现了 Id 的缓冲区。
此外,对于分布式集群部署,UID-Generator 依赖 Mysql 的自增 Id 作为 WorkerId。
最后针对雪花算法的时间回拨问题,DefaultUidGenerator 采取了最简单的抛异常来解决,而 CachedUidGenerator 则是利用其“借用未来时间”特性巧妙解决了时间回拨问题。
总体来说,如果想用雪花算法作为分布式唯一 Id 生成器,那么百度 UID-Generator 是一个不错的选择。


参考:

  1. cloud.tencent.com/developer/a…
  2. github.com/baidu/uid-g…
  3. www.cnblogs.com/yeyang/p/10…
  4. www.cnblogs.com/cyfonly/p/5…
  5. www.cnblogs.com/techyc/p/36…
  6. www.cnblogs.com/zhanjindong…