承接上文雪花算法SnowFlake
介绍
算法描述
指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。
CachedUidGenerator
CachedUidGenerator是UidGenerator的重要改进实现。它的核心利用了RingBuffer,如下图所示,它本质上是一个数组,数组中每个项被称为slot。UidGenerator设计了两个RingBuffer,一个保存唯一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数:
保存唯一ID的RingBuffer有两个指针,Tail指针和Cursor指针。
接下来深入解读CachedUidGenerator的核心操作,即对RingBuffer的操作,包括初始化、取分布式唯一ID、填充分布式唯一ID等。
CachedUidGenerator在初始化时除了给workerId赋值,还会初始化RingBuffer。这个过程主要工作有:
说明:第二步的异步线程实现非常重要,也是UidGenerator解决时钟回拨的关键:在满足填充新的唯一ID条件时,通过时间值递增得到新的时间值(lastSecond.incrementAndGet()),而不是System.currentTimeMillis()这种方式,而lastSecond是AtomicLong类型,所以能保证线程安全问题。
RingBuffer初始化有值后,接下来的取值就简单了。不过,由于分布式ID都保存在RingBuffer中,取值过程中就会有一些逻辑判断:
总结
通过上面对UidGenerator的分析可知,CachedUidGenerator方式主要通过采取如下一些措施和方案规避了时钟回拨问题和增强唯一性:
DefaultUidGenerator
UidGenerator 在应用中是以 Spring 组件的形式提供服务,DefaultUidGenerator 提供了最简单的 Snowflake 式的生成模式,没有使用任何缓存来预存 UID,在需要生成 ID 的时候即时进行计算。
引入DefaultUidGenerator配置
1、epochStr
是给一个过去时间的字符串,作为时间基点,比如"2016-09-20",用于计算时间戳的差值(当前时间减去固定的开始时间),这样可以使产生的 ID 从更小值开始
2、disposableWorkerIdAssigner
Worker ID 分配器,用于为每个工作机器分配一个唯一的 ID,目前来说是用完即弃,在初始化 Bean 的时候会自动向 MySQL 中插入一条关于该服务的启动信息,待 MySQL 返回其自增 ID 之后,使用该 ID 作为工作机器 ID 并柔和到 UID 的生成当中。
buildWorkerNode() 为获取该启动服务的信息,兼容 Docker 服务。但要注意,无论是 docker 还是用 k8s,需要添加相关的环境变量 env 在配置文件中以便程序能够获取到。
核心方法
生成 ID 的核心方法(注意这个方法是同步方法)
可以看到处理异常情况,比如时钟回拨问题,这里的做法比较简单,就是直接抛出异常。
最后一行才是根据传入的或计算好的参数进行 ID 的真正分配,通过二进制的移位和或运算得到最终的 long ID 值。
CachedUidGenerator
CachedUidGenerator 是一个使用 RingBuffer 预先缓存 UID 的生成器,在初始化时就会填充整个 RingBuffer,并在 take() 时检测到少于指定的填充阈值之后就会异步地再次填充 RingBuffer(默认值为 50%),另外可以启动一个定时器周期性检测阈值并及时进行填充
RingBuffer
上面提到 RingBuffer 是预先缓存 UID 的生成器,我们先看下它的成员变量情况:
可以看到 RingBuffer 内部有两个环形数组,一个用来存放 UID,一个用来存放 UID 的状态,这两个数组的大小都是一样的,也就是 bufferSize。
slots 用于存放 UID 的 long 类型数组,flags 用于存放读写标识的 PaddedAtomicLong 类型数组。为什么用 PaddedAtomicLong?上文有提到过伪共享的概念,这里就是为了解决这个问题。
简单讲,由于 slots 实质是属于多读少写的变量,所以使用原生类型的收益更高。而 flags 则是会频繁进行写操作,为了避免伪共享问题所以手工进行补齐。
初始化RingBuffer
RingBuffer 构造方法
bufferSize 的默认值 ,如果 sequence 是 13 位,那么默认最大值是 8192,且是支持扩容的。
触发填充缓冲区的阈值也是支持配置的
RingBuffer 的填充和获取
RingBuffer 的填充和获取操作是线程安全的,但是填充和获取操作的性能会受到 RingBuffer 的大小的影响,先来看下 put 操作:
UID 的读取是一个无锁的操作。在获取 UID 之前,还要检查是否达到了 padding 阈值,在另一个线程中会触发 padding buffer 操作,如果没有更多可用的 UID 可以获取,则应用指定的 RejectedTakeBufferHandler
BufferPaddingExecutor
默认情况下,slots 被消费大于 50%的时候进行异步填充,这个填充由 BufferPaddingExecutor 所执行的,下面看看这个执行者的主要代码。
当线程池分发多条线程来执行填充任务的时候,成功抢夺运行状态的线程会真正执行对 RingBuffer 填充,直至全部填满,其他抢夺失败的线程将会直接返回。
该类还提供定时填充功能,如果有设置开关则会生效,默认不会启用周期性填充
RIngBuffer 的填充时机有 3 个:CachedUidGenerator 时对 RingBuffer 初始化、RIngBuffer#take() 时检测达到阈值和周期性填充(如果有打开)
使用 RingBuffer 的 UID 生成器
最后我们看一下利用 CachedUidGenerator 生成 UID 的代码,CachedUidGenerator 继承了 DefaultUidGenerator,实现了 UidGenerator 接口。
该类在应用中作为 Spring Bean 注入到各个组件中,主要作用是初始化 RingBuffer 和 BufferPaddingExecutor。最重要的方法为 BufferedUidProvider 的提供者,即 lambda 表达式中的 nextIdsForOneSecond(long) 方法。
获取 ID 是通过委托 RingBuffer 的 take() 方法达成的
springboot版本uid-generator源码
https://gitee.com/pingfanrenbiji/java-spring-boot-uid-generator-baidu
参考官方文档
https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
留言与评论(共有 0 条评论) “” |