编程语言应用

首页 » 常识 » 诊断 » 浅谈分布式ID的实践与应用
TUhjnbcbe - 2022/10/31 21:19:00

在业务系统中很多场景下需要生成不重复的ID,比如订单编号、支付流水单号、优惠券编号等都需要使用到。本文将介绍分布式ID的产生原因,以及目前业界常用的四种分布式ID实现方案,并且详细介绍其中两种的实现以及优缺点,希望可以给您带来关于分布式ID的启发。

为什么要用分布式ID

随着业务数据量的增长,存储在数据库中的数据越来越多,当索引占用的空间超出可用内存大小后,就会通过磁盘索引来查找数据,这样就会极大的降低数据查询速度。如何解决这样的问题呢?一般我们首先通过分库分表来解决,分库分表后就无法使用数据库自增ID来作为数据的唯一编号,那么就需要使用分布式ID来做唯一编号了。

分布式ID实现方案

目前,关于分布式ID,业界主要有以下四种实现方案:

UUID:使用JDK的UUID#randomUUID()生成的ID;Redis的原子自增:使用Jedis#incr(Stringkey)生成的ID;Snowflake算法:以时间戳机器号和毫秒内并发组成的64位Long型ID;分段步长:按照步长从数据库读取一段可用范围的ID;我们总结一下这几种方案的特点:

前面两种实现方案的用法以及实现大家日常了解较多,就不在此赘述...本文我们会详细介绍Snowflake算法以及分段步长方案。

Snowflake算法可以做到分配好机器号后就可以使用,不依赖任何第三方服务实现本地ID生成,依赖的第三方服务越少可用性越高,那么我们先来介绍一下Snowflake算法。

Snowflake算法

长整型数字(即Long型数字)的十进制范围是-2^64到2^64-1。

Snowflake使用的是无符号长整型数字,即从左到右一共64位二进制组成,但其第一位是不使用的。所以,在Snowflake中使用的是63bit的长整型无符号数字,它们由时间戳、机器号、毫秒内并发序列号三个部分组成:

时间戳位:当前毫秒时间戳与新纪元时间戳的差值(所谓新纪元时间戳就是应用开始使用Snowflake的时间。如果不设置新纪元时间,时间戳默认是从年开始计算的,设置了新纪元时间可以延长Snowflake的可用时间)。41位2进制转为十进制是2^41,除以(天*24小时*秒*毫秒),约等于69年,所以最多可以使用69年;机器号:10位2进制转为十进制是2^10,即,也就是说最多可以支持有个机器节点;毫秒内并发序列号:12位2进制转为十进制是2^12,即,也就是说一毫秒内在一个机器节点上并发的获取ID,最多可以支持个并发;下面我们来看一下各个分段的使用情况:

那么Snowflake生成的ID长什么样子呢?下面我们来举几个例子(假设我们的时间戳新纪元是-12-:00:00):

Snowflake可以使用三种不同的部署方式来部署,集成分布式部署方式、中心集群式部署方式、直连集群式部署方式。下面我们来分别介绍一下这几种部署方式。

Snowflake集成分布式部署方式

当使用ID的应用节点比较少时,比如个节点以内,适合使用集成分布式部署方式。每个应用节点在启动的时候决定了机器号后,运行时不依赖任何第三方服务,在本地使用时间戳、机器号、以及毫秒内并发序列号生成ID。

下图展示的是应用服务器通过引入jar包的方式实现获取分布式ID的过程。每一个使用分布式ID的应用服务器节点都会分配一个拓扑网络内唯一的机器号。这个机器号的管理存放在MySQL或者ZooKeeper上。

当拓扑网络内使用分布式ID的机器节点很多,例如超过个机器节点时,使用集成部署的分布式ID就不合适了,因为机器号位一共是10位,即最多支持个机器号。当机器节点超过个机器节点时,可以使用下面要介绍的中心集群式部署方式。

Snowflake中心集群式部署方式

中心集群式部署需要新增用来做请求转发的ID网关,比如使用nginx反向代理(即下图中的IDRESTAPIGateway)。

使用ID网关组网后,应用服务器通过HTTP或RPC请求ID网关获取分布式ID。这样相比于上面的集成分布式部署方式,就可以支撑更多的应用节点使用分布式ID了。

如图所示,机器号的分配只是分配给下图中的IDGeneratornode节点,应用节点是不需要分配机器号的。

使用中心集群式部署方式需要引入新的nginx反向代理做网关,增加了系统的复杂性,降低了服务的可用性。那么我们下面再介绍一种不需要引入nginx又可以支持超过个应用节点的直连集群部署方式。

Snowflake直连集群式部署方式

相比于中心集群部署方式,直连集群部署方式可以去掉中间的ID网关,提高服务的可用性。

在使用ID网关的时候,我们需要把IDgeneratornode的服务地址配置在ID网关中。而在使用直连集群式部署方式时,IDgeneratornode的服务地址可以配置在应用服务器本地配置文件中,或者配置在配置中心。应用服务器获取到服务地址列表后,需要实现服务路由,直连ID生成器获取ID。

Snowflake算法存在的问题

Snowflake算法是强依赖时间戳的算法,如果一旦发生时钟回拨就会产生ID重复的问题。那么时钟回拨是怎么产生的,我们又需要怎么去解决这个问题呢?

NTP(NetworkTimeProtocol)服务自动校准可能导致时钟回拨。我们身边的每一台计算机都有自己本地的时钟,这个时钟是根据CPU的晶振脉冲计算得来的,然而随着运行时间的推移,这个时间和世界时间的偏差会越来越大,那么NTP就是用来做时钟校准的服务。

一般情况下发生时钟回拨的概率也非常小,因为一旦出现本地时间相对于世界时间需要校准,但时钟偏差值小于STEP阈值(默认毫秒)时,计算机会选择以SLEW的方式进行同步,即以0.5毫秒/秒的速度差调整时钟速度,保证本地时钟是一直连续向前的,不产生时钟回拨,直到本地时钟和世界时钟对齐。

然而如果本地时钟和世界时钟相差大于STEP阈值时,就会发生时钟回拨。这个STEP阈值是可以修改的,但是修改的越大,在SLEW校准的时候需要花费的校准时间就越长,例如STEP阈值设置为10分钟,即本地时钟与世界时钟偏差在10分钟以内时都会以SLEW的方式进行校准,这样最多会需要14天才会完成校准。

为了避免时钟回拨导致重复ID的问题,可以使用毫秒的STEP阈值,同时在获取SnowflakeID的时候与上一次的时间戳相比,判断时钟回拨是否在1秒钟以内,如果在1秒钟以内,那么等待1秒钟,否则服务不可用,这样可以解决时钟回拨1秒钟的问题。

分段步长方案

Snowflake由于是将时间戳作为长整形的高位,所以导致生成的最小数字也非常大。比如超过时间新纪元1秒钟,机器号为1,毫秒并发序列为1时,生成的ID就已经到了。那么有没有一种方法能够实现在初始状态生成数字较小的ID呢?答案是肯定的,下面来介绍一下分段步长ID方案。

使用分段步长来生成ID就是将步长和当前最大ID存在数据库中,每次获取ID时更新数据库中的ID最大值增加步长。

数据库核心表结构如下所示:

在获取ID时,使用开启事务,利用行锁保证读取到当前更新的最大ID值:

分段步长ID生成方案的优缺点:

优点:ID生成不依赖时间戳,ID生成初始值可以从0开始逐渐增加;缺点:当服务重启时需要将最大ID值增加步长,频繁重启的话就会浪费掉很多分段。针对上述两种实现方案的优化

上文介绍了Snowflake算法以及分段步长方案,他们各有优缺点,针对他们各自的情况我们在本文也给出相应的优化方案。

ID缓冲环

为了提高SnowflakeID的并发性能和可用性,可以使用ID缓冲环(即IDBufferRing)。提高并发性提现在通过使用缓冲环能够充分利用毫秒时间戳,提高可用性提现在可以相对缓解由时钟回拨导致的服务不可用。缓冲环是通过定长数组加游标哈希实现的,相比于链表会不需要频繁的内存分配。

在ID缓冲环初始化的时候会请求ID生成器将ID缓冲环填满,当业务需要获取ID时,从缓冲环的头部依次获取ID。当ID缓冲环中剩余的ID数量少于设定的阈值百分比时,比如剩余ID数量少于整个ID缓冲环的30%时,触发异步ID填充加载。异步ID填充加载会将新生成的ID追加到ID缓冲环的队列末尾,然后按照哈希算法映射到ID缓冲环上。另外有一个单独的定时器异步线程来定时填充ID缓冲环。

下面的动画展示了ID缓冲环的三个阶段:ID初始化加载、ID消费、ID消费后填充:

BufferRingInitializeload,ID缓冲环初始化加载:从IDgenerator获取到ID填充到ID缓冲环,直到ID缓冲环被填满;BufferRingconsume,ID缓冲环消费:业务应用从ID缓冲环获取ID;Asyncreload,异步加载填充ID缓冲环:定时器线程负责异步的从IDgenerator获取ID添加到ID缓冲队列,同时按照哈希算法映射到ID缓冲环上,当ID缓冲环被填满时,异步加载填充结束;

下面的流程图展示了ID缓冲环的运行的整个生命周期,其中:

IDConsumerServer:表示使用分布式ID的业务系统;IDBufferRing:ID缓冲环;IDGenerator:ID生成器;IDBufferRingAsyncLoadThread:异步加载ID到缓冲环的线程;Timer:负责定时向异步加载线程添加任务来装载ID;ID消费流程:即上面提到的BufferRingconsume;整体流程:客户端业务请求到应用服务器,应用服务器从ID缓冲环获取ID,如果ID缓冲环内空了那么抛出服务不可用;如果ID缓冲环内存有ID那么就消费一个ID。同时在消费ID缓冲环中的ID时,如果发现ID缓冲环中存留的ID数量少于整个ID缓冲环容量的30%时触发异步加载填充ID缓冲环。

ID双桶Buffer

在使用分段步长ID时,如果该分段的ID用完了,需要更新数据库分段最大值再继续提供ID生成服务,为了减少数据库更新查询可能带来的延时对ID服务的性能影响,可以使用双桶缓存方案来提高ID生成服务的可用性。

其主要原理:设计两个缓存桶:currentBufferBucket和nextBufferBucket,每个桶都存放一个步长这么多的ID,如果当前缓存桶的ID用完了,那么就将下一个缓存桶设置为当前缓存桶。

下面的动画展示了双桶缓存初始化、异步加载预备桶和将预备桶切换成当前桶的全过程:

Currentbucketinitialload:初始化当前的缓存桶,即更新max=max+step,然后获取更新后的max值,比如步长是,更新后的max值是,那么桶的高度就是步长即,桶min=max-step+1=1,max=;Currentbucketremainingidcountdownto20%,Nextbucketstarttoload。当前缓存桶的ID剩余不足20%的时候可以加载下一个缓存桶,即更新max=max+step,后获取更新后的max值,此时更新后的max值是0,min=max-step+1=,max=0;Currentbucketisdrained,Switchcurrentbuckettothenextbucket,如果当前桶的ID全部用完了,那么就将下一个ID缓存桶设置为当前桶;

下面是双桶Buffer的流程图:

总结

本文主要介绍了分布式ID的实现方案,并详细介绍了其中Snowflake方案和分段步长方案,以及针对这两种方案的优化方案。我们再简单总结一下两个方案:

在高并发场景下生成大量的分布式ID,适合使用Snowflake算法方案,毫秒内并发序列为2^12=,单机QPS支持高达4百万,但是需要对ID生成器的机器号进行管理;使用分段步长方式生成ID就可以免去对机器号的管理,但是需要合理的设置步长,如果步长太短满足不了并发需求,如果步长太长又会造成分段的过渡浪费;以上就是本文的全部内容,如果有更多关于分布式ID的技术也欢迎留言与我们交流。

作者介绍

古德,网易云信资深JAVA开发工程师。现在负责网易会议账户体系、互动直播等模块的设计与研发。对微服务、分布式事务等中间件技术方面有一定的经验。热爱技术喜欢Coding,善于面向对象设计编程、领域驱动设计与代码优化重构。

1
查看完整版本: 浅谈分布式ID的实践与应用