百度分布式id生产器UidGenerator

前言

UidGenerator是百度开源的Java语言实现,基于Snowflake算法的惟一ID生成器。并且,它很是适合虚拟环境,好比:Docker。另外,它经过消费将来时间克服了雪花算法的并发限制。UidGenerator提早生成ID并缓存在RingBuffer中。 压测结果显示,单个实例的QPS能超过6000,000。依赖环境:node

  • JDK8+
  • MySQL(用于分配WorkerId)

snowflake

雪花算法几个核心部分: 算法

  • 1位sign标识位;
  • 41位时间戳;
  • 10位workId(数据中心+工做机器,能够其余组成方式);
  • 12位自增序列;

百度分布式id生成器作了修改: 数据库

时间部分是28位,意味着默认只能承受8.5年(2^28-1/86400/365)。根据不一样业务需求,能够适当调整delta seconds,worker node id和sequence占用位数。 UidGenerator提供两种方式:DefaultUidGenerator 和 CachedUidGenerator 。数组

DefaultUidGenerator

delta seconds缓存

指的是当前时间和epoch时间的时间差,单位位秒。epoch时间指的是UidGenerator生成分布式ID服务第一次上线的时间,可配置,也必定要根据你的上线时间进行配置,由于默认的epoch时间但是2016-09-20,不配置的话,会浪费好几年的可用时间。安全

worker id服务器

看下worker id是如何赋值的,先建立一个表:数据结构

DROP TABLE IF EXISTS WORKER_NODE;
CREATE TABLE WORKER_NODE(
  ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
  HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',
  PORT VARCHAR(64) NOT NULL COMMENT 'port',
  TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
  LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',
  MODIFIED DATETIME NOT NULL COMMENT 'modified time',
  CREATED DATEIMTE NOT NULL COMMENT 'created time'
)
 COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;

分布式ID的实例启动的时候,往这个表中插入一行数据,获得的id值就是准备赋给workerId的值。因为workerId默认22位,那么,集成UidGenerator生成分布式ID的全部实例重启次数是不容许超过4194303次(即2^22-1),不然会抛出异常。 固然也能够自定义生成workerid的方式。并发

**sequence **异步

关键点实现:

  • synchronized保证线程安全;
  • 若是时间有任何的回拨,那么直接抛出异常;
  • 若是当前时间和上一次是同一秒时间,那么sequence自增。若是同一秒内自增值超过2^13-1,那么就会自旋等待下一秒(getNextSecond);
  • 若是是新的一秒,那么sequence从新从0开始;
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);
    }
    if (currentSecond == lastSecond) {
        sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
        if (sequence == 0) {
            currentSecond = getNextSecond(lastSecond);
        }
    } else {
        sequence = 0L;
    }
    lastSecond = currentSecond;
    return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}

总结

时钟回拨的处理比较简单粗暴。另外若是使用UidGenerator的DefaultUidGenerator方式生成分布式ID,必定要根据你的业务的状况和特色,调整各个字段占用的位数:

<property name="timeBits" value="28"/>
<property name="workerBits" value="22"/>
<property name="seqBits" value="13"/>
<property name="epochStr" value="2016-09-20"/>

CachedUidGenerator

CachedUidGenerator是UidGenerator的重要改进实现。它的核心利用了RingBuffer,以下图所示,它本质上是一个数组,数组中每一个项被称为slot。UidGenerator设计了两个RingBuffer,一个保存惟一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数:

RingBuffer Of Flag

保存flag这个RingBuffer的每一个slot的值都是0或者1,0是CAN_PUT_FLAG的标志位,1是CAN_TAKE_FLAG的标识位。每一个slot的状态要么是CAN_PUT,要么是CAN_TAKE。以某个slot的值为例,初始值为0,即CAN_PUT。接下来会初始化填满这个RingBuffer,这时候这个slot的值就是1,即CAN_TAKE。等获取分布式ID时取到这个slot的值后,这个slot的值又变为0,以此类推。

RingBuffer Of UID

保存惟一ID的RingBuffer有两个指针,Tail指针和Cursor指针。

Tail指针表示最后一个生成的惟一ID。若是这个指针追上了Cursor指针,意味着RingBuffer已经满了。这时候,不容许再继续生成ID了。用户能够经过属性rejectedPutBufferHandler指定处理这种状况的策略。

Cursor指针表示最后一个已经给消费的惟一ID。若是Cursor指针追上了Tail指针,意味着RingBuffer已经空了。这时候,不容许再继续获取ID了。用户能够经过属性rejectedTakeBufferHandler指定处理这种异常状况的策略。

另外,若是你想加强RingBuffer提高它的吞吐能力,那么须要配置一个更大的boostPower值:

<!-- RingBuffer size扩容参数, 可提升UID生成能力.即每秒产生ID数上限能力 --> 
<!-- 默认:3,原bufferSize=2^13, 扩容后bufferSize = 2^13 << 3 = 65536 -->
<property name="boostPower" value="3"/>

CachedUidGenerator的理论讲完后,接下来就是它具体是如何实现的了,咱们首先看它的申明,它是实现了DefaultUidGenerator,因此,它事实上就是对DefaultUidGenerator的加强:

public class CachedUidGenerator extends DefaultUidGenerator implements DisposableBean {
   ... ...
}

worker id

CachedUidGenerator的workerId实现继承自它的父类DefaultUidGenerator,即实例启动时往表WORKER_NODE插入数据后获得的自增ID值。

接下来深刻解读CachedUidGenerator的核心操做,即对RingBuffer的操做,包括初始化、取分布式惟一ID、填充分布式惟一ID等。

初始化

CachedUidGenerator在初始化时除了给workerId赋值,还会初始化RingBuffer。这个过程主要工做有:

  • 根据boostPower的值肯定RingBuffer的size;
  • 构造RingBuffer,默认paddingFactor为50。这个值的意思是当RingBuffer中剩余可用ID数量少于50%的时候,就会触发一个异步线程往RingBuffer中填充新的惟一ID(调用BufferPaddingExecutor中的paddingBuffer()方法,这个线程中会有一个标志位running控制并发问题),直到填满为止;
  • 判断是否配置了属性scheduleInterval,这是另一种RingBuffer填充机制, 在Schedule线程中, 周期性检查填充。默认:不配置, 即不使用Schedule线程. 如需使用, 请指定Schedule线程时间间隔, 单位:秒;
  • 初始化Put操做拒绝策略,对应属性rejectedPutBufferHandler。即当RingBuffer已满, 没法继续填充时的操做策略。默认无需指定, 将丢弃Put操做, 仅日志记录. 若有特殊需求, 请实现RejectedPutBufferHandler接口(支持Lambda表达式);
  • 初始化Take操做拒绝策略,对应属性rejectedTakeBufferHandler。即当环已空, 没法继续获取时的操做策略。默认无需指定, 将记录日志, 并抛出UidGenerateException异常. 若有特殊需求, 请实现RejectedTakeBufferHandler接口;
  • 初始化填满RingBuffer中全部slot(即塞满惟一ID,这一步和第2步骤同样都是调用BufferPaddingExecutor中的paddingBuffer()方法);
  • 开启buffer补丁线程(前提是配置了属性scheduleInterval),原理就是利用ScheduledExecutorService的scheduleWithFixedDelay()方法。

说明:第二步的异步线程实现很是重要,也是UidGenerator解决时钟回拨的关键:在知足填充新的惟一ID条件时,经过时间值递增获得新的时间值(lastSecond.incrementAndGet()),而不是System.currentTimeMillis()这种方式,而lastSecond是AtomicLong类型,因此能保证线程安全问题。

取值

RingBuffer初始化有值后,接下来的取值就简单了。不过,因为分布式ID都保存在RingBuffer中,取值过程当中就会有一些逻辑判断:

  • 若是剩余可用ID百分比低于paddingFactor参数指定值,就会异步生成若干个ID集合,直到将RingBuffer填满。
  • 若是获取值的位置追上了tail指针,就会执行Task操做的拒绝策略。
  • 获取slot中的分布式ID。
  • 将这个slot的标志位只为CAN_PUT_FLAG。

总结

经过上面对UidGenerator的分析可知,CachedUidGenerator方式主要经过采起以下一些措施和方案规避了时钟回拨问题和加强惟一性:

  • 自增列:UidGenerator的workerId在实例每次重启时初始化,且就是数据库的自增ID,从而完美的实现每一个实例获取到的workerId不会有任何冲突。
  • RingBuffer:UidGenerator再也不在每次取ID时都实时计算分布式ID,而是利用RingBuffer数据结构预先生成若干个分布式ID并保存。
  • 时间递增:传统的雪花算法实现都是经过System.currentTimeMillis()来获取时间并与上一次时间进行比较,这样的实现严重依赖服务器的时间。而UidGenerator的时间类型是AtomicLong,且经过incrementAndGet()方法获取下一次的时间,从而脱离了对服务器时间的依赖,也就不会有时钟回拨的问题(这种作法也有一个小问题,即分布式ID中的时间信息可能并非这个ID真正产生的时间点,例如:获取的某分布式ID的值为3200169789968523265,它的反解析结果为{"timestamp":"2019-05-02 23:26:39","workerId":"21","sequence":"1"},可是这个ID可能并非在"2019-05-02 23:26:39"这个时间产生的)。
相关文章
相关标签/搜索