【转载】雪花算法与时间回拨
原文地址 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 最佳,这可能更多的取决于使用场景。

浙公网安备 33010602011771号