浅谈分布式 ID 的实践与应用

在业务系统中不少场景下须要生成不重复的 ID,好比订单编号、支付流水单号、优惠券编号等都须要使用到。本文将介绍分布式 ID 的产生缘由,以及目前业界经常使用的四种分布式 ID 实现方案,而且详细介绍其中两种的实现以及优缺点,但愿能够给您带来关于分布式 ID 的启发node

为何要用分布式 ID

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

分布式 ID 实现方案

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

  • UUID:使用 JDK 的 UUID#randomUUID() 生成的 ID;
  • Redis 的原子自增:使用 Jedis#incr(String key) 生成的 ID;
  • Snowflake 算法:以时间戳机器号和毫秒内并发组成的 64 位 Long 型 ID;
  • 分段步长:按照步长从数据库读取一段可用范围的 ID;

咱们总结一下这几种方案的特色:sql

方案   顺序性   重复性   可用性   部署方式   可用时间  
UUID   无序   经过多位随机字符达到极低重复几率,但理论上是会重复的   一直可用   JDK 直接调用   永久  
Redis   单调递增   RDB 持久化模式下,会出现重复   Redis 宕机后不可用   Jedis 客户端调用   永久  
Snowflake   趋势递增   不会重复   发生时钟回拨而且回拨时间超过等待阈值时不可用   集成部署、集群部署   69年  
分段步长   趋势递增   不会重复   若是数据库宕机而且获取步长内的 ID 用完后不可用 集成部署、集群部署   永久  

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

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

Snowflake 算法

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

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

  • 时间戳位:当前毫秒时间戳与新纪元时间戳的差值(所谓新纪元时间戳就是应用开始使用 Snowflake 的时间。若是不设置新纪元时间,时间戳默认是从1970年开始计算的,设置了新纪元时间能够延长 Snowflake 的可用时间)。41 位 2 进制转为十进制是 2^41,除以(365 天 * 24 小时 * 3600 秒 * 1000 毫秒),约等于 69年,因此最多可使用 69 年;
  • 机器号:10 位 2 进制转为十进制是 2^10,即 1024,也就是说最多能够支持有 1024 个机器节点;
  • 毫秒内并发序列号:12 位 2 进制转为十进制是 2^12,即 4096,也就是说一毫秒内在一个机器节点上并发的获取 ID,最多能够支持 4096 个并发;

下面咱们来看一下各个分段的使用状况:服务器

二进制分段   [1]   [2, 42]   [43, 52]   [53, 64]  
说明   最高符号位不使用   一共41位,是毫秒时间戳位   一个10位,是机器号位   一共12位,是毫秒内并发序列号,当前请求的时间戳若是和上一次请求的时间戳相同,那么就将毫秒内并发序列号加一  

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

时间 机器号 毫秒并发 十进制 Snowflake ID
2021-01-01 08:33:11 1 10 491031363588106
2021-01-02 13:11:12 2 25 923887730696217
2021-01-03 21:22:01 3 1 1409793654796289

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

Snowflake 集成分布式部署方式

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

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

集成部署的分布式ID

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

Snowflake 中心集群式部署方式

中心集群式部署须要新增用来作请求转发的 ID 网关,好比使用 nginx 反向代理(即下图中的 ID REST API Gateway)。

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

如图所示,机器号的分配只是分配给下图中的 ID Generator node 节点,应用节点是不须要分配机器号的。

中心化部署的分布式ID

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

Snowflake 直连集群式部署方式

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

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

直连集群部署的分布式ID

Snowflake 算法存在的问题

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

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

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

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

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

分段步长方案

Snowflake 因为是将时间戳做为长整形的高位,因此致使生成的最小数字也很是大。好比超过期间新纪元 1 秒钟,机器号为 1,毫秒并发序列为 1 时,生成的 ID 就已经到 4194308097 了。那么有没有一种方法可以实如今初始状态生成数字较小的 ID 呢?答案是确定的,下面来介绍一下分段步长 ID 方案。

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

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

CREATE TABLE `segment_id` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `biz_type` varchar(64) NOT NULL DEFAULT '', // 业务类型
  `max` bigint(20) DEFAULT '0', // 当前最大 ID 值
  `step` bigint(20) DEFAULT '10000', // ID 步长
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8           

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

start transaction;
update segment_id set max = max + step where biz_type = 'ORDER';
select max from segment_id where biz_type = 'ORDER';
commit

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

  • 优势:ID 生成不依赖时间戳,ID 生成初始值能够从 0 开始逐渐增长;
  • 缺点:当服务重启时须要将最大 ID 值增长步长,频繁重启的话就会浪费掉不少分段。

针对上述两种实现方案的优化

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

ID 缓冲环

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

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

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

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

Buffer Ring

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

  • IDConsumerServer:表示使用分布式 ID 的业务系统;
  • IDBufferRing:ID 缓冲环;
  • IDGenerator:ID 生成器;
  • IDBufferRingAsyncLoadThread:异步加载 ID 到缓冲环的线程;
  • Timer:负责定时向异步加载线程添加任务来装载 ID;
  • ID 消费流程:即 上面提到的 Buffer Ring consume;

总体流程:客户端业务请求到应用服务器,应用服务器从 ID 缓冲环获取 ID,若是 ID 缓冲环内空了那么抛出服务不可用;若是 ID 缓冲环内存有 ID 那么就消费一个 ID 。同时在消费 ID 缓冲环中的 ID 时,若是发现 ID 缓冲环中存留的 ID 数量少于整个 ID 缓冲环容量的 30% 时触发异步加载填充 ID 缓冲环。

ID Buffer Ring

ID 双桶 Buffer

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

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

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

  • Current bucket initial load:初始化当前的缓存桶,即更新 max = max + step,而后获取更新后的 max 值,好比步长是 1000,更新后的 max 值是 1000,那么桶的高度就是步长即 1000,桶 min = max - step + 1 = 1,max = 1000;
  • Current bucket remaining id count down to 20%,Next bucket start to load。当前缓存桶的 ID 剩余不足 20% 的时候能够加载下一个缓存桶,即更新 max = max + step,后获取更新后的 max 值,此时更新后的 max 值是 2000,min = max - step + 1 = 1001, max = 2000;
  • Current bucket is drained,Switch current bucket to the next bucket,若是当前桶的 ID 所有用完了,那么就将下一个 ID 缓存桶设置为当前桶;

DoubleBucketBuffer

下面是双桶 Buffer 的流程图:

双桶Buffer

总结

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

  • 在高并发场景下生成大量的分布式 ID,适合使用 Snowflake 算法方案,毫秒内并发序列为2^12=4096,单机 QPS 支持高达 4 百万,可是须要对 ID 生成器的机器号进行管理;
  • 使用分段步长方式生成 ID 就能够免去对机器号的管理,可是须要合理的设置步长,若是步长过短知足不了并发需求,若是步长太长又会形成分段的过渡浪费;

以上就是本文的所有内容,若是有更多关于分布式 ID 的技术也欢迎留言与咱们交流。

做者介绍

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

相关文章
相关标签/搜索