前言
假如面试时候,问到分布式唯一ID生成的相关问题,我觉得snowflake应绕不开,既然绕不开,那怎么回答呢?
简洁介绍
64位的二进制表示,分了4块
第1块不用管,0是固定的,表示正数。
核心就在后3块。
第2块:
当前时间戳 减 开始时间戳,单位毫秒。
开始时间一般指定位项目上线日期就行。
很多博客都简略的说“时间戳”,包括图片中也是。
很容易混淆,这里存的可不是你用xx语言的一个time相关的API调用得出的那个值。
第3块:
10位,范围 0~1023 = 1024
工作机器ID,就是机器的唯一标志,或者具体点说实例的唯一标志,随便取,不超出位数范围,互相不重复就行。
第4块:
自增序列号,0~4095 = 4096
这3部分的分配只是官方建议的,我们可以根据实际需要自行分配,但是各部分的作用是不变的。
时钟回退
笔者耗费了一天时间,并未找到确切解决时钟回退的方案。
网上一些标题党,不是复制粘贴,就是方案存在漏洞或限制。笔者决定放弃。
笔者认为时钟回退可以分为2种情况
个别机器的微小误差
比如运维写个脚本统一纠正所有机器的时钟,就有那么一些机器,差个三两秒,甚至几毫秒。
其实如果机器真的就差那么点时间,不纠正也可以,那么既然纠正了,其实也是可以有方案应对的。
1、等一等
当判断出现时钟回退情况,可以进行一个短暂的等待,几毫秒后再判断,还是不行,就抛异常。
这种方案,只能解决小部分的微小误差,作用有限。
2、吃回头草
以snowflake为例,同一毫秒同一机器,最多能get出4096个ID,可是你们真的有那么大的并发量么?
换句话说,过去的那些时间,都百分百用完了4096个序列号了么?
那么此时,我们可以记录最近的一段时间的毫秒数和使用了的最大序列号 map<时间戳,max序列号>
此时,当发生时钟回退的时候,我们可以利用之前的时间剩余的那些序列号,保证请求不报错。
这个方案的问题就是,1、分布式ID的时间不准确 2、对于回退较长时间,map还是不够用 3、
3、去隔壁借碗饭
(这个方案是笔者自己想到的,不清楚为什么网上都没人提过,也可能是我没看到)
(大学旁边的小饭馆一条街,经常出现,你去一家店里点饭菜,可是这家生意太好了,米饭卖光了,
这时候饭店老板娘通常会去隔壁饭店买米饭。总不能因为米饭卖没了,这单生意就不做了吧)
那么依此,为什么我们的思维要局限在一个机器上呢?一个机器发生时钟回退了,那就去其他机器上“借一个ID”先用着呗!
总不可能整个集群,所有机器全部都时钟退回了吧,那无解了。
集体回退 / 回退过长
你别管是怎么原因,集群中所有机器,集体时钟回退,或者某一些机器,回退时长过长。
这种情况真的不知道怎么办了,也许直接抛异常,报警是最直接的。
也许解决时钟回拨的问题就是不用时钟
《UidGenerator:百度开源的分布式ID服务(解决了时钟回拨问题)》
这篇文章标题,去搜搜,很多博客都是原模原样的照搬照抄,标题看起来挺唬人的。
其实按照他的说法,也可以说是解决了时钟回拨问题,根本原因是没用时钟进行运算。
没用时钟就不存在时钟回拨问题喽!!!
snowflake的4块中的第二块,说到底就是个数,即使你把它定义为时间戳,也是用2个时间戳相减的差值,
之所以用时间戳,就是因为这样生产的ID是有意义的。
而CachedUidGenerator显然是抛弃了这点,果然,“既要...又要...”、“两手抓两手都要硬”这些都是梦。
笔者简述一下这个过程,并不是CachedUidGenerator的实现,只是笔者对于这个方案的描述和理解。
(实操起来考虑点非常多,至少CachedUidGenerator中用到的RingBuffer,笔者并不理解,哎,留下了没有技术的泪水)
过程是这样的:
比如我们设置基础时间,一般是这个分布式ID生成器项目上线日期,比如 2020-10-10 00:00:00
而第2块的数值 = now() - ‘2020-10-10 00:00:00’ = 差值
现在 now()会受到 时钟回拨的影响,那我们就不计算了,我们自己生成这个‘差值’
比如第一个‘差值’是0,第4块有n个序号,那我就基于这个‘差值’预先生成n个ID,缓存起来,get请求进来,直接从缓存中拿,
拿的差不多了(比如缓存里还剩50%了),‘差值’+1 = 1,又能生成n个序号,给缓存满上,再拿一些,再满上,以此类推。
这种分布式ID看起来一点问题没有,非常的snowflake,也不受时钟回拨的影响,唯一的问题就是它的时间是“假的”,并不是ID生成的真正时间。
这个方案顺带着解决了另一个问题(算是笔者的理解)
你看啊,以snowflake为例,理论上,同一毫秒,第4块能用4096个数,可是你们实际生产生活中,能用上这么些么?
对于一些没什么并发的业务来说,所拿到的ID末尾大部分都是0,就是说大部分是 "0xxxxyyy0"这种情况,
因为没啥毫秒级的并发,每个时间点去拿,都从0开始,造成了ID大部分都是偶数,对于分库分表来说,可能会受一些影响
而用上述方案,就不会,都是从 0~4095排满的,非常的“雨露均沾”。