公众号后台回复“
面试
”,获取精品学习资料
扫描下方海报了解
专栏详情
本文来源:
阿飞的博客
《Java工程师面试突击(第3季)》重磅升级,由原来的
70讲增至160讲
,内容扩充一倍多,升级部分内容请参见文末
UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。而且,它非常适合虚拟环境,比如:Docker。
另外,它通过消费未来时间克服了雪花算法的并发限制。UidGenerator提前生成ID并缓存在RingBuffer中。压测结果显示,单个实例的QPS能超过6000,000。
依赖环境:
-
JDK8+
-
MySQL(用于分配WorkerId)
snowflake
由下图可知,雪花算法的几个核心组成部分:
-
1位sign标识位;
-
41位时间戳;
-
10位workId(数据中心+工作机器,可以其他组成方式);
-
12位自增序列;
snowflake
但是百度对这些组成部分稍微调整了一下:
baidu uid generator
由上图可知,UidGenerator的时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365)
当然,根据你业务的需求,UidGenerator可以适当调整delta seconds、worker node id和sequence占用位数。
接下来分析百度UidGenerator的实现。需要说明的是UidGenerator有两种方式提供:和DefaultUidGenerator和CachedUidGenerator。我们先分析比较容易理解的DefaultUidGenerator。
DefaultUidGenerator
这个值是指当前时间与epoch时间的时间差,且单位为秒。
epoch时间就是指集成UidGenerator生成分布式ID服务第一次上线的时间,可配置,也
一定
要根据你的上线时间进行配置,因为默认的epoch时间可是2016-09-20,不配置的话,会浪费好几年的可用时间。
接下来说一下UidGenerator是如何给worker id赋值的,搭建UidGenerator的话,需要创建一个表:
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;
UidGenerator会在集成用它生成分布式ID的实例启动的时候,往这个表中插入一行数据,得到的id值就是准备赋给workerId的值。
由于workerId默认22位,那么,集成UidGenerator生成分布式ID的所有实例重启次数是不允许超过4194303次(即2^22-1),否则会抛出异常。
这段逻辑的核心代码来自DisposableWorkerIdAssigner.java中,当然,你也可以实现WorkerIdAssigner.java接口,自定义生成workerId。
核心代码如下,几个实现的关键点:
protected synchronized long nextId() {
long currentSecond = getCurrentSecond();
if
(currentSecond 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);
}
通过DefaultUidGenerator的实现可知,它对时钟回拨的处理比较简单粗暴。
另外如果使用UidGenerator的DefaultUidGenerator方式生成分布式ID,一定要根据你的业务的情况和特点,调整各个字段占用的位数:
"timeBits" value="28"/>
"workerBits" value="22"/>
"seqBits" value="13"/>
"epochStr" value="2016-09-20"/>
CachedUidGenerator
CachedUidGenerator是UidGenerator的重要改进实现。它的核心利用了RingBuffer,如下图所示
它本质上是一个数组,数组中每个项被称为slot。UidGenerator设计了两个RingBuffer,一个保存唯一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数:
RingBuffer
其中,保存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,以此类推。
保存唯一ID的RingBuffer有两个指针,Tail指针和Cursor指针。
-
Tail指针表示最后一个生成的唯一ID。如果这个指针追上了Cursor指针,意味着RingBuffer已经满了。这时候,不允许再继续生成ID了。用户可以通过属性rejectedPutBufferHandler指定处理这种情况的策略。
-
Cursor指针表示最后一个已经给消费的唯一ID。如果Cursor指针追上了Tail指针,意味着RingBuffer已经空了。这时候,不允许再继续获取ID了。用户可以通过属性rejectedTakeBufferHandler指定处理这种异常情况的策略。
另外,如果你想增强RingBuffer提升它的吞吐能力,那么需要配置一个更大的boostPower值:
<property name="boostPower" value="3"/>
CachedUidGenerator的理论讲完后,接下来就是它具体是如何实现的了,我们首先看它的申明,它是实现了DefaultUidGenerator,所以,它事实上就是对DefaultUidGenerator的增强:
public class CachedUidGenerator extends DefaultUidGenerator implements DisposableBean {
... ...
}
CachedUidGenerator的workerId实现继承自它的父类DefaultUidGenerator,即实例启动时往表WORKER_NODE插入数据后得到的自增ID值。
接下来深入解读CachedUidGenerator的核心操作,即对RingBuffer的操作,包括初始化、取分布式唯一ID、填充分布式唯一ID等。