背景
我们需要什么样的ID生成器
- 高性能 -> 1.生成性能高 2.插入性能高 3.索引性能高
- 高可用 -> 1.依赖中间件要少 2.避免单节点问题
- 不重复 -> 1.集群内全局不重复
- 易使用 -> 1.接入简单 2.零学习成本
-
如何满足这几点要求
生成性能高 -> 大多数时间为内存分配,减少IO,减少锁
- 插入性能高 -> 全局递增,避免页分裂
- 索引性能高 -> 数值类型
- 全局不重复 -> 分布式一致性
-
总结特性
全局递增
- 数字类型
- 全局唯一
- 无锁并发
- 内存分配
- 单机生成(大多数时间)
SnowFlake
Twitter把存储系统从MySQL迁移到Cassandra,因为Cassandra没有顺序ID生成机制,所以开发了这样一套全局唯一ID生成服务。Ray的基本思想来自于SnowFlake,解决了一些SnowFlake中存在的一些问题
如何保证单机递增不重复
最简单的思路:时间戳+序列号
伪代码:If 当前时间 > 上次ID生成时间 -> 当前时间+序列号0
If 当前时间 = 上次ID生成时间 -> 当前时间 + 上次序列号+1
If 当前时间 < 上次ID生成时间 -> 是否存在这种情况
如何保证全局不重复
如何判断两个ID是重复的?我们认为两个ID每一位都是重复的则两个ID重复。
两个重复的数值如果分别拼接上不同的数值,则最终这两个数值不相同,如:
1111和1111,分别拼上1和2
则1111-1和1111-2是不相同的
之前我们在通过时间戳+序列号实现单机内不重复
那么我们只需要保证单机内不重复的ID + 不重复的实例ID(workId)就能保证最终生成的ID不重复
如何保证workId不重复
这是一个分布式一致性问题
这个可以使用分布式锁实现每个实例独占一个workId
并且这仅在启动时和续约时会依赖中间件
即使依赖的中间件中间暂时不可用,只是新的服务不能使用,旧的正常
阶段性总结
- 全局递增(时间戳自带全局递增)-> 插入,索引高性能
- 内存分配(分配时无IO) -> 生成高性能
- 单机生成(仅启动时强依赖中间件)
->
高可用
SnowFlake存在的问题
时钟偏斜问题
现代计算机至少有两种不同的时钟:时钟和单调钟。尽管它们都衡量时间,但区分这两者很重要,因为它们有不同的目的。时钟
它根据某个日历返回当前日期和时间。例如: Java中的System.currentTimeMillis()返回自epoch(1970年1月1日 午夜 UTC,格里高利历)以来的秒数(或毫秒),根据公历日历,不包括闰秒。单调钟
适用于测量持续时间(时间间隔),例如Java中的System.nanoTime()都是单调时钟。这个名字来源于他们保证总是前进的事实。问题
时钟的问题在于,虽然它们看起来简单易用,但却具有令人惊讶的缺陷:一天可能不会有精确的86,400秒,时钟可能会前后跳跃,而一个节点上的时间可能与另一个节点上的时间完全不同。
如果时间往前拨我们就无法确保时间戳+序列号生成的ID是单机唯一的。单位毫秒内生成数上限
SnowFlake单位毫秒内生成的ID数不能超过12位的序列位问题总结
也就是说SnowFlake严重依赖于当前时间戳,并且只能处理当前时间戳大于等于上次时间戳的情况,对于当前时间戳小于上次时间戳的情况无法处理。解决时钟偏斜问题
If 当前时间 > 上次ID生成时间 -> 当前时间+序列号0
If 当前时间 = 上次ID生成时间 -> 当前时间 + 上次序列号+1
If 当前时间 < 上次ID生成时间 ->上次ID生成时间 + 上次序列号 +1
时钟偏斜,时间前跳其实就是等价于当前时间 < 上次ID生成时间
如何记录上次ID生成时间
首先上次ID生成时间记录在当前实例内存中
如果时钟回拨发生在重启时,上次ID生成时间记录会丢
此时新实例拿到了它的workId,并且新机器的时间戳更早就会出现ID重复的情况
所以需要后台定期同步全局最大的时间戳到中间件,实例退出和启动前同步时间戳
无尽的序列号
SnowFlake为什么会出现序列号只有4096位的情况?
1.位数就那么多,你可以从机器位上分配
2.没有进位的空间,只支持当前时间戳>=上次时间戳
Ray支持当前时间<上次时间
意味着4096位用完可以往时间戳上进位,解决突发流量
Ray
Ray参考了SnowFlake的设计,解决了时钟回调和序列号不足的问题,并且提供了更好的并发性能
Github地址:https://github.com/KeshawnVan/Ray