记一次雪花算法的实现

1,827 阅读6分钟

作者最近在实现雪花算法生成主键,看了不少文章。个人觉得雪花算法的最大的优点是有序(减少数据库的页分裂页合并)。雪花算法本身很简单,但他依赖于时间,也就是机器或容器的时间一回拨就出现问题了。网上也出现了不少关于如何解决回拨的问题。我记录一下本次实现雪花主键的过程。

先定个目标

  • 实现雪花算法的主键
  • 提供不同策略的时钟回拨解决方法
  • 尽量避免完全自增,导致ID规则被发现

初识雪花算法

雪花算法生成的id的结果是一个64bit的整数,结构如下(引用网上的图片):

​ 由此可以看出他是一个较大的正整数,在同一毫秒内,他可以生成4096个ID(12位序列号,12个1的二进制就是4095,再加个0)。所以适合并发量较大的有序ID。

​ 由于有41位的时间戳,所以非常依赖应用程序的时间,如果时钟发生回拨,会造成前面41位的二进制重复,从而导致ID重复,这是不可取的。

时间回拨解决思路

思路一

​ 我们的目标是想让ID有序(自增也行)。但我们的并发不可能一直很高,几乎不可能每一毫秒都是4096个插入。所以肯定有相对来说有一毫秒内用不完的序列。所以当时我就做了一个想法,我的时间戳部分能不能等这个毫秒用完了序列号再增加一毫秒。等时钟回拨的时候,我不看当前时间,我只看到了哪个时间戳。这种方式能解决时间回拨问题,但有个致命缺陷。因为你的ID永远是有规律的。太容易被爬走了。如果你以这个ID作为商家ID或其它相对公开信息的ID,很容易被人爬走信息。这种想法暂时放弃。

思路二

​ 假设一般情况下我们不会随意回拨赶时间,这也确实是的,谁也不会没事找事把时间改来改去,特别是生产上。所以我们假设时间回拨很小,回拨的几率也很小,而且这个回拨的时候在客户的忍耐范围之内。比如回拨了1秒钟,我们的应用允许有2秒的请求延迟,所以这个在我们的容忍范围内,我可以让时间过了这一秒,再生成主键。这是可取的。我们称这种策略叫"等待",也叫WAITING

​ 这种策略仅限于客户或应用可以容忍一定时间内的时钟回拨,如果回拨超时一定时间内,我们可以抛出异常,让程序响应异常。

​ 此种策略应用于数据量大,但并发不是特别大的情况。

思路三

​ 我们仔细观察ID生成规则,会发现有一个10位的工作机器ID,我们常常把他分为两块:前5位是datacenterId,也有人叫roomId,我们这里叫他groupId;后5位是workerId,就是具体的工作ID,这两块可以有1024种组合。如果我们不是大型微服务应用,这个1024个组合用不完,那我们能不能拿出一部分作为备用组合。这样在时间回拨的时候启用备用组合。这种叫“启用备用ID”,我们叫Extra workerId

​ 这种策略仅限于工作机器的ID组合用不完,并且时钟不会回拨到同一时间点或区间的情况。如果我们在一个时间点(假设是1)一开始是由主workerId生成ID,后来由于时钟回拨,又跳回时间点1,我们启用备用extra workerId。但是又发生时钟回拨,又将时钟跳回时间点1,这个时候workerIdextra workerId都用过了,在这个时间点已经没有任何workerId可用了,所以必然会造成ID重复。

​ 这种策略可用于workerId用不完,且不会回拨至同一个时间点(区间)的情况。

思路四

​ 我们如果只是用当前时间戳,作为前41位,因为位数固定,所以相对来说这个ID使用的时间较短。但是如果我们减去一个差值(这里叫偏移量),用相对时间,这样我们使用的时候就会长很多。因为减去一部分,使ID变小,这样ID的增长空间才会变大,使用时间变长。

​ 好,我们假设也引入了偏移量这个概念。如果时钟回拨,会产生什么现象呢?时钟一回拨ID变小,导致ID会重复,所以我们这个时候只要想办法让ID再次变大,变成和回拨前一样的大小或比回拨前稍微大一点。那如何处理呢,刚刚我们引进了偏移量这个概念,时钟回拨我们会让ID变小,但减小偏移量我们会让ID变大,所以我们通过修改偏移量的方式达到让ID回到原来的轨道上来。

​ 肯定又有人说了你在内存里修改了偏移量,下次重启的时候咋办,应用肯定会重启啊。是的,如果应用重启了,如果重启的间隔时间大于回拨时间。这个时候是没有问题,如果小于,会出现ID重复的情况。这个时候我们还有办法吗?答案是有的。我们可以在时钟回拨后将偏移量存起来,然后项目重启后再加载进来。这样就可以了。

​ 这种策略可以用于并发量很大的场景。对于同一张表同一个应用他的存储一定是一个,不能乱。否则可能会出现ID重复。

总结

​ 我们提出以上几点时钟回拨的解决思路。看似解决了一些问题。可是问题又来了。对于一个应用来说有可能有不同表都需要生成ID。这个时候我们来回顾一下,雪花算法的组成部分:时间戳,机器ID,12位序列号。

  • 时间戳:对于同一个应用来说只有一个,所以是共用的

    • 时间偏移量,共用,一旦时钟回拨,所有ID生成器的偏移量都要变小。理论上保存的时候只要保存一份即可。但实际上最好是多份,因为不是所有ID生成器都能发现时钟回拨,或者发现时钟回拨的时间不一样,这个时候建议保存多份。
  • 机器ID:对于同一个应用来说也是一样的,所以也是共用的

    • 备用机器ID,共用,一旦时钟回拨,所有ID生成器的备用ID都要切到备用机器ID,但实际上也建议是多份,不是所有ID生成器都能发现时钟回拨。
  • 序列号:不同表的序列号肯定不一样。所以为每张表(ID生成器)定义一个squence

    本次作者就写到这里。相关代码已经上传至: github.com/zhouxx/boot… 模块之boot-plus-mybatis-jpa的主键实现部分。欢迎一起讨论。