分析雪花算法的思想 | 青训营笔记

233 阅读7分钟

这是我参与「第三届青训营 -后端场」笔记创作活动的第4篇笔记

1、分布式ID的基本要求

  • 全局不重复(根本要求)
  • 按时间趋势递增(适应数据库存储策略)
  • 高性能(防止生成ID这个动作成为瓶颈)

2、雪花算法的思想

64位,每一位的作用:

  • 第一个部分:1位,没有具体含义,只是用来表示每个ID都是正数
  • 第二个部分:41位,时间戳,精确到毫秒,可以支持使用到2082年,可以说不存在上限溢出问题
  • 第三个部分:10位,表示机器ID。为了方便配置,5位表示机房号,5位表示机器号
  • 第四个部分:12位,表示序列号,用于区分同一毫秒内产生的不同ID,支持每毫秒产生4096个ID

此处体现了这些设计思想:

  • 实现趋势递增:在高位使用时间戳,所以生成的ID虽然不是连续递增,但可以满足随时间递增
  • 同一个时间戳内,区分不同机器产生的ID:规定每台机器的唯一编号,即机房号+机器号
  • 同一台机器上,区分同一个时间戳内产生的ID:用低12位来做区分

3、雪花算法存在的问题

上面研究了最基本的雪花算法思想,如果不采用任何已有的优化方案,它目前存在几个问题:

  • 使用时间戳来实现趋势递增很聪明,但是不经处理的时间戳存在一些问题
  • 序列号长度有限,虽然每台机器每秒支持产生4096个不重复的ID,但如果发生了上溢,就会导致产生重复ID,这决定了系统的并发量

3.1、关于时间戳

我提供一个最简单的雪花算法实现方案:

  • 使用语言自带的API,读取宿主机的本地系统时间,生成毫秒级的时间戳
  • 配置好了机器ID,这看做一个常量
  • 提供一个线程安全的自增方法,它能返回一个12位的自增数字,用作序列号
  • 每次需要生成ID,就获取时间戳,读取机器ID,生成一个自增的序列号,然后利用位运算把它们移动到规定的位置,一个雪花ID就生成好了

我们再来回顾一下雪花算法需要满足的两个特点:

  • 绝对不能重复
  • 趋势递增

可以发现:

  • 机器ID是人为配置的,它只起到区分作用,单单靠它不能实现这两个特点
  • 序列号是自增的,但是它是每毫秒归零一次,也就是说宏观来看,它也起不到不重复和递增这两个作用
  • 所以雪花算法的核心功能都是由时间戳来保证的。

那么时间戳自身存在这些问题:

  • 我这个雪花算法每次都是读取本地时间,如果有人篡改了本地时间,那就有风险产生重复的ID,而且无法满足趋势递增了
  • 一个分布式系统中,每台机器的时钟肯定会存在微小的差异,这就可能导致生成的ID不一定都满足趋势递增
  • 获取时间戳这个动作本身不能太复杂,否则一定会成为雪花算法的性能瓶颈

3.2、时钟回拨问题

为什么会有时钟回拨问题

  • 有人篡改了宿主机的系统时间
  • 集群中可能会进行整体的时钟同步,从而修改机器的本地时间

时钟回拨对雪花算法的影响

时钟回拨问题,我的理解在上面提到过,就是如果篡改了本地时间,那就有风险产生重复的ID,而且无法满足趋势递增了。

不递增的问题稍小,但如果产生了重复的ID,这就可能产生严重的后果。

把这个问题具象出一个场景:

  • 雪花算法作为一个独立的服务,部署在一台机器上,让它持续生成2分钟的雪花ID,理论上它能把这个时间范围内全部的ID都生成出来
  • 机器掉电,我篡改系统时间为1分钟之前,那么之后这台机器产生的雪花ID,肯定会和之前的有重复

时钟回拨的解决思路

对于这个问题,我的分析如下:

  • 方案一:想办法探测到时钟回拨,然后做出对应的策略

  • 方案二:探索一种ID生成的方式,不完全依靠时间戳来保证雪花算法,或者直接使用别的策略替代时间戳

    • 使用自增序列代替时间戳,好处是可以充分利用全部的序列号,满了之后再去自增“时间戳”的部分,坏处是不同机器的自增序列相差可能很大,导致生成的ID做不到全局趋势递增

其中方案一分为两个部分:

  • 如何探测到时钟回拨
  • 知道当前发生了时钟回拨,那么怎么办?

如何探测到时钟回拨

要知道当前获取到的时间戳是否符合之前的整体趋势,那肯定要保存历史时间戳数据。

简单的做法:

  • 保存一个全局变量,存放上一次获取到的时间戳
  • 每次生成ID前,先获取本地时间戳,然后和上一次保存的作比较,如果当前的小于上一次的,说明发生了时钟回拨

这种方式存在的问题:

  • 只保存一份之前的时间戳,它不一定是可靠的,因为可能发生了多次时钟回拨,所以还得保存一段时间的时间戳来验证整体的趋势,这就变得更复杂了
  • 解释一下单个历史时间戳不可靠的原因:比如当前是5点,已经生成了一批5点的ID,时钟拨到3点,探测到发生了时间回拨,但是再拨到4点就探测不到了。但我们知道,其实5点之前的ID都已经生成过一次了,这次从4点开始生成,就有生成出重复ID的风险。

但问题是,我们无法知道时钟回拨发生的具体时机,难道把服务上线以来所有的历史时间戳都保留起来,然后每次生成ID之前都需要遍历这么多的数据,去查看历史时间戳是否都小于当前时间戳?肯定不可以的。

那么按这个思路,只保存历史时间戳中的最大值是否可以?也不行,比如我把时间调整到2082年,那么整个雪花算法就无法使用了,因为时钟恢复正常后,生成的时间戳肯定都小于这个2082年。

发生时钟回拨后的策略

简单的做法:

  • 死循环,等到当前时间戳大于历史时间戳后,再去生成ID。肯定不可取,如果时间相差过大,相当于服务直接挂掉
  • 利用序列号,如果上一个时间戳的序列号还没有用完,就缓存起来,当前依然使用上次时间戳,外加上次的序列号递增,来生成当前的ID

4、部署方式对雪花算法的影响

有两种部署方式:

  • 单独的发号器服务,做集群保证高可用,但是每次获取ID都要经过网络,耗时肯定更长
  • 本地部署,把雪花算法和业务部署在一起,本地直接获取ID,效率高,但是需要专门维护机器ID

5、一个优化思路

时间戳的缓存思路

  • 获取本地时间时,涉及到系统调用,而且效率不是很高
  • 在高并发场景,每毫秒需要生成几千个ID,那么就需要获取几千次系统时间,引入缓存机制后,肯定比发起几千次系统调用的效率要高
  • 这个缓存实现的前提是,能评估当前的并发量,因为如果并发量很低,就没必要为了少数几个ID去构建缓存,更新缓存

\