【转载】雪花算法与时间回拨

原文地址 www.modb.pro,by 茶叶蛋日常

标记一个调用,一笔日志,一笔交易,经常会需要生成一个唯一 ID,而生成唯一 ID 的形式有很多,常常使用的,便是 UUID (Universally Unique Identifier)。UUID 十分方便,无需网络,效率也高,Java 等语言都提供了相关接口,很容易就能得到一个全球唯一的 ID。

2a7b067b-ea89-4bb5-9aa6-51596d4d53c0

UUID 的生成规则里包含了硬件标识、时间戳、计数器等信息。但却有个比较不爽的点,就是 UUID 的长度,即使用 16 进制表示,依然有 32 位之长,这增加了传输以及存储的成本。

自增序列或许是最直接的一种形式,设计一个计数器,每次获取便自增,这样可以保证序号唯一,且逻辑简单,效率高。这在单机器下十分方便,但考虑到分布式场景,自增序列计数器的位置便不那么简单。倘若各个机器各放置一个,则可能产生相同的 ID,而倘若将自增值存储在数据库,Zookeeper 或者 Redis 上,则增加了网络开销。若是出现不合理的锁争用等情况,那获取一个序号花上十几毫秒,便不那么好接受。

考虑将时间戳作为唯一 ID。比如 Java 中,可以取 System.currentTimeMillis(),即 1970-1-1 日 0 点到现在的毫秒值,以当前的时间来计算,大概得到一个递增的 13 位的十进制数字。但这是一个不怎么明智的选择。假若 CPU 效率高,在同一毫秒内序号生成被调用了多次,则会获得相同的 ID。那么考虑在时间戳的基础上,增加一个 N 位的自增序列,记录每一毫秒生成的 ID 数呢,那又回到了自增序列存放位置的问题。

而即使自增序列的问题解决了,又怎么保证时间戳是一往无前的呢?如果保证选择是 0 时 0 分 0 秒,下一瞬间不会变成 23 时 59 分 59 秒呢?

事实上时间回拨是可能发生的。除却一些异常场景,一天的时间并不是那么准确的 24X60x60=86400 秒,地球有时候转得快,有时候转得慢。而电脑的时钟(也许是晶振吧)却是相对比较稳定的。这就导致了每隔一段时间就会出现闰秒。这对日常生活可能没什么影响,但表现在计算机上,那就是时间回溯了。那这种情况下,按我们的算法,ID 不就重复生成了么?

那么,倘若是分布式场景下,我们要生成一个唯一 ID,首先我们不希望生成一个 ID 耗费的资源太多,至少不要有什么网络开销。那必然我们需要在 ID 的生成规则里,加入机器的硬件标识。

在这个基础上,使用时间戳加自增序号是一个比较容易想到的形式。但时间回拨的问题,对一些业务系统不是那么容易接受。

考虑解决时间回拨的问题,为了知道当前时间被回拨了。我们需要一个标志位,记录上一次生成 ID 的时间,每次生成 ID 的时候,都跟这个时间戳进行比较,如果当前时间戳在这之前,则表示发生了时间回拨。当得知发现时间回拨的时候,如何处理呢?

直接抛出异常,等待程序员处理?虽然是小概率事件,但不应该是序号生成器编码人员应该忽略的问题。

线程进入等待,直到当前时间戳大于上一时间戳?也许是个方式,但加入回拨了一秒,那就需要程序等待一秒,这对于实时性及可用性要求比较高的流程,可能不是很能接受。

比较常用的方式是在出现时间回拨时,使用上一时间戳的序号来继续累加。比如上一秒时 0 时 0 分 0 秒,自增序号到达了 x。因为时间回拨,下一秒时间是 23 时 59 分 59 秒,那么下一次生成序号,发现当前时间戳小于上一时间戳时,则使用 0 时 0 分 0 秒的,自增序号使用 x+1。但,虽然概率不高,自增序号用完后,是不是又得重新等待呢?

或者,我们也可以使用一个虚拟分配的硬件标志,或者一个不可能发生的时间位置,在发生时间回拨的时间区间内,用来生成唯一 ID。

硬件标志、时间戳、序号,这种序号生成方式,当前最流行的莫过于雪花算法了。经典的雪花算法,使用

0+41 位毫秒级时间戳 + 10 位硬件标志 + 12 位序号

总共 64 位的二进制生成。

雪花算法相对于 UUID 而言,存储的长度仅为一半,更方便的是,通过生成的 ID 可以倒推出一些信息,则 ID 呈自增实行,不会像 UUID 一样摸不着头脑。

而上面的方式,也就是多种处理了时间回拨的雪花方法。网上改进雪花算法的方式很多,但实际如何改进雪花算法来生成 ID 最佳,这可能更多的取决于使用场景。

posted @ 2023-02-18 19:51  洗衣机o  阅读(145)  评论(0)    收藏  举报