分布式SnowFlakeID(雪花ID)原理和改进优化

最近在研究分布式框架的组件和总体设计思路。全部的问题,一旦涉及分布式难度就呈几何倍数的提高。包括最多见的ID生成也是,单机状况下,使用数据库自增ID、UUID都是简单易行的选择html

但在分布式环境下,就须要考虑同业务部署多套之后,ID重复的问题。使用数据库则数据库容易成为瓶颈,使用UUID又没有顺序,数据库集成又会遇到递增步长等问题。最后,数据库(也可以使用redis)号段生成器和snowFlake就成为了目前分布式ID生成器的主流mysql

我所知大部分互联网公司的分布式ID生成器,其实都是一个网络服务或集群,单独部署。其余应用程序经过网络去获取分布式的全局惟一ID。使用网络服务的方式,好处显而易见,就是方便集中管理,只要生成器设计的没问题,基本ID就能保证总体趋势是递增的。坏处就是获取效率被明显下降了git

另外针对我司来讲,因为项目的性质,采用分布式ID生成器,对开发和上线部署及其后期的运维都会带来必定的麻烦。毕竟上线后,项目的管理权就不在咱们手上了,因此为了保证分布式ID生成器的稳定性,尽可能不采起分布式ID生成中心的策略。因而,留给个人选择就只剩下了SnowFlakeID(雪花ID)了。github

什么是SnowFlakeID

SnowFlake是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评。由这种算法生成的ID,咱们就叫作SnowFlakeID面试

SnowFlakeID的最大的特性就是自然去中心化,经过时间戳、工做机器编号两个变量进行配置后,经过SnowFlake算法会生成惟一的递增ID。在任何机器上,只要保证工做机器编号不一样,就能够确保生成的ID惟一,且总体趋势是递增的redis

Snowflake的结构以下(每部分用-分开):算法

0 - 0000000000 0000000000 0000000000 0000000000 0 - 0000000000 - 000000000000

第一段1位为未使用,永远固定为0sql

第二段41位为毫秒级时间(41位的长度可使用69年)数据库

第三段10位为workerId(10位的长度最多支持部署1024个节点)服务器

第三段12位为毫秒内的计数(12位的计数顺序号支持每一个节点每毫秒产生4096个ID序号)

若是按照1024的满节点(1个节点就是1个部署的服务)计算,每毫秒可生成的ID序号有1024*4096=4194304个,足以知足如今绝大多数的业务状况

算法的核心以下

((当前时间 - 服务时间) << timestampLeftShift) 
        | (机器ID << workerIdShift) 
        | sequence;

服务时间指的是服务的开发时间,即第一个正式ID产生的时间。因为SnowFlakeID最长可用69年(由于只有41个bit,41个bit的最大值换算成年就是69年)。因此服务时间越贴近上线时间,则该算法可用时间越长。 其中sequence为递增序列,当前时间戳和上一ID生成时间戳一致时,sequence就递增1,直到4096为止。

SnowFlake有什么问题

SnowFlake很好,分布式、去中心化、无第三方依赖。但它并非完美的,因为SnowFlake强依赖时间戳,因此时间的变更会形成SnowFlake的算法产生错误。

时钟回拨:最多见的问题就是时钟回拨致使的ID重复问题,在SnowFlake算法中并无什么有效的解法,仅是抛出异常。时钟回拨涉及两种状况①实例停机→时钟回拨→实例重启→计算ID ②实例运行中→时钟回拨→计算ID

手动配置:另外一个就是workerId(机器ID)是须要部署时手动配置,而workerId又不能重复。几台实例还好,一旦实例达到必定量级,管理workerId将是一个复杂的操做。

如何优化

时钟回拨改进避免

ID生成器一旦不可用,可能形成全部数据库相关新增业务都不可用,影响太大。因此时钟回拨的问题必须解决。

形成时钟回拨的缘由多种多样,多是闰秒回拨,多是NTP同步,还多是服务器时间手动调整。总之就是时间回到了过去。针对回退时间的多少能够进行不一样的策略改进。通常有如下几种方案:

  1. 少许服务器部署ID生成器实例,关闭NTP服务器,严格管理服务器。这种方案不须要从代码层面解决,彻底人治。
  2. 针对回退时间断的状况,如闰秒回拨仅回拨了1s,能够在代码层面经过判断暂停必定时间内的ID生成器使用。虽然少了几秒钟可用时间,但时钟正常后,业务便可恢复正常。
if (refusedSeconds <= 5) {
    try {
    //时间误差大小小于5ms,则等待两倍时间
		wait(refusedSeconds << 1);//wait
	} catch (InterruptedException e) {
		e.printStackTrace();
	}
    currentSecond = getCurrentSecond();
}else {//时钟回拨较大
    //用其余策略修复时钟问题
}
  1. 实例启动后,改用内存生成时间。该方案为baidu开源的UidGenerator使用的方案,因为实例启动后,时间再也不从服务器获取,因此无论服务器时钟如何回拨,都影响不了SnowFlake的执行。以下代码中lastSecond变量是一个AtomicLong类型,用以代替系统时间
List<Long> uidList = uidProvider.provide(lastSecond.incrementAndGet());
  1. 以上2和3都是解决时钟实例运行中→时钟回拨→计算ID的状况。而实例停机→时钟回拨→实例重启→计算ID的状况,能够经过实例启动的时候,采用未使用过的workerId来完成。只要workerId和此前生成ID的workerId不一致,即使时间戳有误,所生成的ID也不会重复。UidGenerator采起的就是这种方案,但这种方案又必须依赖一个存储中心,不论是redis、mysql、zookeeper均可以,但必须存储着此前使用过的workerId,不能重复。尤为是在分布式部署Id生成器的状况下,更要注意用一个存储中心解决此问题。
  2. UidGenerator代码可上Githubhttps://github.com/zer0Black/uid-generator查看

手动配置如何变为自动

其实该处的方案和时钟回拨的第四个方案是一致的,每次重启实例的时候,自动的查找workerId使用,不依赖手动配置。且自查找的workerId不会重复。方便管理。

参考文档

  1. UidGenerator文档
  2. 一口气说出 9种 分布式ID生成方式,面试官有点懵了
  3. Leaf——美团点评分布式ID生成系统

原文出处:https://www.cnblogs.com/zer0Black/p/12323541.html

相关文章
相关标签/搜索