分布式ID生成器Ray

4,434 阅读5分钟

背景

在应用程序中,经常需要全局唯一的ID作为数据库主键。

我们需要什么样的ID生成器

  • 高性能 -> 1.生成性能高 2.插入性能高 3.索引性能高
  • 高可用 -> 1.依赖中间件要少 2.避免单节点问题
  • 不重复 -> 1.集群内全局不重复
  • 易使用 -> 1.接入简单 2.零学习成本
  • 语义性 -> 1.ID带有一些其它信息

如何满足这几点要求

  • 生成性能高 -> 大多数时间为内存分配,减少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地址:github.com/KeshawnVan/…