这是我参与「第三届青训营 -后端场」笔记创作活动的第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去构建缓存,更新缓存
\