百度开源的分布式唯一ID生成器UidGenerator,解决了时钟回拨问题
UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器。而且,它非常适合虚拟环境,比如:Docker。另外,它通过消费未来时间克服了雪花算法的并发限制。UidGenerator提前生成ID并缓存在RingBuffer中。 压测结果显示,单个实例的QPS能超过6000,000。
依赖环境:
- JDK8+
- MySQL(用于分配WorkerId)
snowflake
由下图可知,雪花算法的几个核心组成部分:
- 1位sign标识位;
- 41位时间戳;
- 10位workId(数据中心+工作机器,可以其他组成方式);
- 12位自增序列;
但是百度对这些组成部分稍微调整了一下:
由上图可知,UidGenerator的时间部分只有28位,这就意味着UidGenerator默认只能承受8.5年(2^28-1/86400/365)。当然,根据你业务的需求,UidGenerator可以适当调整delta seconds、worker node id和sequence占用位数。
接下来分析百度UidGenerator的实现。需要说明的是UidGenerator有两种方式提供:和DefaultUidGenerator和CachedUidGenerator。我们先分析比较容易理解的DefaultUidGenerator。
DefaultUidGenerator
delta seconds
这个值是指当前时间与epoch时间的时间差,且单位为秒。epoch时间就是指集成UidGenerator生成分布式ID服务第一次上线的时间,可配置,也一定要根据你的上线时间进行配置,因为默认的epoch时间可是2016-09-20,不配置的话,会浪费好几年的可用时间。
worker id
接下来说一下UidGenerator是如何给worker id赋值的,搭建UidGenerator的话,需要创建一个表:
UidGenerator会在集成用它生成分布式ID的实例启动的时候,往这个表中插入一行数据,得到的id值就是准备赋给workerId的值。由于workerId默认22位,那么,集成UidGenerator生成分布式ID的所有实例重启次数是不允许超过4194303次(即2^22-1),否则会抛出异常。
这段逻辑的核心代码来自DisposableWorkerIdAssigner.java中,当然,你也可以实现WorkerIdAssigner.java接口,自定义生成workerId。
sequence
核心代码如下,几个实现的关键点:
- synchronized保证线程安全;
- 如果时间有任何的回拨,那么直接抛出异常;
- 如果当前时间和上一次是同一秒时间,那么sequence自增。如果同一秒内自增值超过2^13-1,那么就会自旋等待下一秒(getNextSecond);
- 如果是新的一秒,那么sequence重新从0开始;
总结
通过DefaultUidGenerator的实现可知,它对时钟回拨的处理比较简单粗暴。另外如果使用UidGenerator的DefaultUidGenerator方式生成分布式ID,一定要根据你的业务的情况和特点,调整各个字段占用的位数:
CachedUidGenerator
CachedUidGenerator是UidGenerator的重要改进实现。它的核心利用了RingBuffer,如下图所示,它本质上是一个数组,数组中每个项被称为slot。UidGenerator设计了两个RingBuffer,一个保存唯一ID,一个保存flag。RingBuffer的尺寸是2^n,n必须是正整数:
RingBuffer Of Flag
其中,保存flag这个RingBuffer的每个slot的值都是0或者1,0是CANPUTFLAG的标志位,1是CANTAKEFLAG的标识位。每个slot的状态要么是CANPUT,要么是CANTAKE。以某个slot的值为例,初始值为0,即CANPUT。接下来会初始化填满这个RingBuffer,这时候这个slot的值就是1,即CANTAKE。等获取分布式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值:
CachedUidGenerator的理论讲完后,接下来就是它具体是如何实现的了,我们首先看它的申明,它是实现了DefaultUidGenerator,所以,它事实上就是对DefaultUidGenerator的增强:
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的标志位只为CANPUTFLAG。
总结
通过上面对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"这个时间产生的)。
百度开源的分布式唯一ID生成器UidGenerator,解决了时钟回拨问题的更多相关文章
- 分布式唯一id生成器的想法
0x01 起因 前端时间遇到一个问题,怎么快速生成唯一的id,后来采用了hashid的方法.最近在网上读到了美团关于分布式唯一id生成器的解决方案, 其中提到了三种生成法:(建议看一下这篇文章,写得很 ...
- snowflake 分布式唯一ID生成器
本文来自我的github pages博客http://galengao.github.io/ 即www.gaohuirong.cn 摘要: 原文参考运维生存和开源中国上的代码整理 我的环境是pytho ...
- 分布式唯一ID生成器Twitter
分布式系统中,有一些需要使用全局唯一ID的场景,这种时候为了防止ID冲突可以使用36位的UUID,但是UUID有一些缺点,首先他相对比较长,另外UUID一般是无序的. 有些时候我们希望能使用一种简单一 ...
- 分布式唯一ID生成器
在应用程序中,经常需要全局唯一的ID作为数据库主键.如何生成全局唯一ID? 首先,需要确定全局唯一ID是整型还是字符串?如果是字符串,那么现有的UUID就完全满足需求,不需要额外的工作.缺点是字符串作 ...
- 百度开源的分布式 id 生成器
UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器.UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于d ...
- 分布式全局ID生成器原理剖析及非常齐全开源方案应用示例
为何需要分布式ID生成器 **本人博客网站 **IT小神 www.itxiaoshen.com **拿我们系统常用Mysql数据库来说,在之前的单体架构基本是单库结构,每个业务表的ID一般从1增,通过 ...
- springboot 集成百度的唯一ID生成器
UidGenerator是百度开源的Java语言实现,基于Snowflake算法的唯一ID生成器.而且,它非常适合虚拟环境,比如:Docker.另外,它通过消费未来时间克服了雪花算法的并发限制.Uid ...
- 开源项目|Go 开发的一款分布式唯一 ID 生成系统
原文连接: 开源项目|Go 开发的一款分布式唯一 ID 生成系统 今天跟大家介绍一个开源项目:id-maker,主要功能是用来在分布式环境下生成唯一 ID.上周停更了一周,也是用来开发和测试这个项目的 ...
- 分布式唯一id:snowflake算法思考
匠心零度 转载请注明原创出处,谢谢! 缘起 为什么会突然谈到分布式唯一id呢?原因是最近在准备使用RocketMQ,看看官网介绍: 一句话,消息可能会重复,所以消费端需要做幂等.为什么消息会重复后续R ...
随机推荐
- python科学计算和数据分析常用库
NumPy NumPy最强大的是n维数组,该库还包含基本的线性代数函数.傅立叶变换.随机函数和其他底层语言(如Fortran.C和C++)集成的工具. SciPy SciPy建立在NumPy基础上,它 ...
- [PHP] 接口增加recaptcha行为验证
需要先翻墙创建一个谷歌账户和创建recaptcha验证的网站域名,获取到两个secrecthttps://www.google.com/recaptcha/admin 前端增加html和js代码,例如 ...
- Logstash filter 插件之 date
使用 date 插件解析字段中的日期,然后使用该日期或时间戳作为事件的 logstash 时间戳.对于排序事件和导入旧数据,日期过滤器尤其重要.如果您在事件中没有得到正确的日期,那么稍后搜索它们可能会 ...
- form表单中的button自动刷新页面问题
form表单中如果存在button的话,有可能会出现一个问题:点击button,触发了页面的自动刷新事件. 原因是因为<button>标签默认的类型是submit,即默认的button点击 ...
- Linux下Mysql安装教程详解
Linux下软件安装一般有三种方式:RPM包方式(通过Redhat 第三方包管理系统).二进制包和源码包.本篇主要介绍二进制包安装mysql数据库的方式. 如何获取二进制源码包 当然是到mysql官网 ...
- Kafka简明教程
作者:柳树之 www.jianshu.com/p/7b77723d4f96 Kafka是啥?用Kafka官方的话来说就是: Kafka is used for building real-time d ...
- 10分钟彻底理解Redis的持久化机制:RDB和AOF
作者:张君鸿 juejin.im/post/5d09a9ff51882577eb133aa9 什么是Redis持久化? Redis作为一个键值对内存数据库(NoSQL),数据都存储在内存当中,在处理客 ...
- TypeScript `this` 入参
考察下面的示例代码: class MyClass { constructor(protected foo: string) {} @MyDecorator bar() { console.log(&q ...
- C# 异步转同步 PushFrame
异步转同步-PushFrame 本文通过PushFrame,实现异步转同步 首先有一个异步方法,如下异步任务延时2秒后,返回一个结果 private static async Task<stri ...
- linu运行级别
一.介绍 0:关机 1:单用户[找回丢失密码] 2:多用户状态[无网络服务] 3:多用户状态[有网络服务] 4:保留级别 5:图形界面 6:系统重启 二.命令行运行级别 比如说关机 init 0 三. ...