分布式ID生成如何解决

本文涉及的产品
服务治理 MSE Sentinel/OpenSergo,Agent数量 不受限
简介: 笔记

在日常的业务开发中,通常需要对一些数据做唯一标识,例如为大量抓取的文章入库时分配一个唯一的id,为用户下的订单分配订单号等等。并发量小的时候,通常会使用数据库自增的主键id作为唯一id。并发量大的时候就会考虑使用一些分布式ID的生成方案来生成id。由于一些特殊的业务需求,我们的业务中也使用到了分布式ID的生成,对分布式ID的各种方案进行了调研。也对开源的分布式ID框架进行了一些优化和改造。本文主要分为以下三个部分:

一.常见的分布式ID生成方案的简单介绍。

二.对当前开源的分布式ID框架实现原理进行分析,例如美团的Leaf和百度的uid-generator。

三.Leaf和uid-generator存在的一些问题及如何进行优化改进。

一、分布式ID生成方案简介

目前常用的分布式ID生成方案主要由以下几种:

1.使用UUID算法生成唯一id。

2.利用单机数据库主键自增来生成唯一id。

3.多数据库主键自增生成唯一id。(设置步长区分不同数据库)

4.数据库分段发号生成唯一id。(例如美团的Leaf框架中的segement模式)

5.基于snowflake算法生成唯一id(例如美团的Leaf框架中的snowflake模式,百度的uid-generator)

1.UUID

简单的来说,UUID是服务器在不需要任何外界依赖(像类Snowflake算法的方案都需要注册中心)的情况下,基于当前时间、计数器(counter)和硬件标识等等信息生成的唯一ID。

优点

无任何依赖

其他的技术方案都是有依赖的,比如单机数据库主键自增生成ID强依赖数据库,类Snowflake算法的方案至少启动时都需要注册中心,Leaf框架Snowflake模式需要定时上传时间戳到注册中心,的UUID生成不需要任何外界依赖,

缺点

ID太长,且不是数字类型

当然为了唯一性,带来的牺牲就是生成的结果一般是32位的字符串。由于字符串太长,并且不是数字类型,所以不适合作为数据库的主键。

(字符串作为主键id,插入数据时会是在聚集索引中是随机插入,容易造成页分离。而且字符串的比较比数字类型的开销更大,字符串作为主键id查询效率会低于数字类型的主键。)

适用场景

通常可以作为一些临时性唯一标识,例如用户登陆后,生成一个UUID作为登录的会话ID,作为key存储在Redis中,Value是用户相关的信息。

2.单机数据库主键自增

业务量不大时普遍采用这种方案来生成id。

优点

方便接入

因为一般的项目不一定会用到Zookeeper等这些组件,但是基本都会用到数据库,所以项目接入会比较简单,也没有增加额外的维护成本。

单调递增

是绝对的单调递增的,就是从时间线上看,后面生成的id肯定比前面生成的id要大。

缺点

强依赖于数据库

一旦单机数据库发生宕机,就没法生成id,导致整个系统不可用。如果数据库是主从架构的,主库发生故障,切换成从库,如果从库还没来得及收到主库最新的插入id的更新,就有可能导致从库当前的自增id不是最新的,从而生成出重复的id。

id是连续的

id是连续的有可能会成为缺点,竞争对手在当天12点下一个订单,然后在第二天12点下一个订单,可能根据订单id的差就可以推测出每天的订单量。像猫眼电影就使用了这种方法来生成电影的id。一般我们日常使用时,其实为了让我们生成id的方式更难被竞争对手猜测出,一般是不会从1开始的,但是猫眼电影这里是从1开始的,而且是连续的,所以我们使用二分法很快就确定了8875是最大的值,也就是总共有8875部电影,而且由于是连续的,爬取也会比较方便。 猫眼电影的id——也是使用单机数据库生成的,连续自增的

https://maoyan.com/films/1
https://maoyan.com/films/2
https://maoyan.com/films/8874
https://maoyan.com/films/8875
https://maoyan.com/films/8876
(1到8875可以请求到数据,8876及后面的id
请求不到数据,没有这些id对应的电影,说明总共有8875部电影)

除非去做额外的处理(例如定时去获取当前的自增起始值a,然后生成一个随机数b,使用alter table users AUTO_INCREMENT=a+b;命令对自增起始值修改,也就是跳过一些id。)

适用场景

适用于并发量不高的业务。

3.多数据库主键自增生成唯一id。(设置步长区分不同数据库)

可以使用以下命令设置MySQL中表每次自增时的步长,通过将不同数据库的步长设置为一样,可以让不同数据库生成的id进行区分。

CREATE TABLE table (...) AUTO_INCREMENT = n;
alter table <table name> auto_increment=2;

例如,步长是等于数据库的数量,例如有N台数据库, 第一台数据库的起始值是0,那么生成的id就是0,N,2N,3N等等。

第一台数据库的起始值是1,那么生成的id就是1,N+1,2N+1,3N+1等等。 这样各个数据库生成的id就不会冲突,并且每个数据库可以单独生成id。

优点

生成id的效率比单台数据库要高,因为可以多台数据库同时发号。

缺点

是解决了每次自增是1的问题,缺点是一旦设置了步长,就不方便扩容了,因为分库分表的表的数量已经定下来了。

使用场景

在没有分库分表的框架以前,那些分库分表就是使用这种方案来实现的。

4.数据库分段发号生成唯一id。(例如美团的Leaf框架中的segement模式)

简单来说,就是想下图一样,用一个数据库表来充当发号器, 表中字段介绍如下:

biz_tag字段用于区分每种id应用的业务,
max_id字段记录了当前已生成的最大的id,
step字段代表每次可以获取id的数量

id生成项目每次使用下面这条语句从数据库获取step数量的id,并且更新max_id的值,将step数量的id存储在内存中,供业务方通过HTTP,RPC,Client等方式来获取。

UPDATE leaf_alloc SET max_id = max_id + step WHERE biz_tag = #{tag}

优点

效率高

生成id的效率取决于step的大小,不会像主键自增生成id那样再受限于数据库的数量。

缺点

强依赖于数据库

还是强依赖于数据库,数据库宕机后,虽然id生成系统靠内存中还未使用完id,可以维持系统正常运行一段时间,但是数据库不可用还是会导致整个系统不可用。

5.基于snowflake算法生成唯一id

snowflake是推特开源分布式ID生成算法,一共有64位,

第一位是0,标志位

接下来41位是13位的毫秒时间戳,最大可以到2039年9月

接下来10个二进制位是服务器的id

后面12位是业务序列号

意味着每毫秒最大可以生成2的12次方个id,4096个,支持每个机器每毫秒生成4096个id,每秒可以生成400多万的id

优点

效率高

生成id是比较快,

不依赖其他组件

生成id的过程中,可以做到不额外依赖其他组件。

二、开源的分布式ID框架实现原理分析

Leaf

Leaf主要有Leaf-Segment和Leaf-Snowflake两种模式

Leaf-Segment

Leaf-Segment主要采用上面的第四种.数据库分段发号生成唯一id的技术方案实现的。

这是我画的一个执行流程图,左边部分是业务系统获取id的流程:

1.业务系统通过HTTP,RPC,Client等方式向ID生成系统获取ID。

2.判断ID生成系统内存中当前SegmentBuffer使用率是否达到了10%,是的话触发另一个SegmentBuffer的异步更新。 (SegmentBuffer可以认为是一个容器,用于存放每次从数据库取出的step数量的id,然后供业务系统消耗。)

3.判断当前SegmentBuffer的id是否使用完了,如果没有使用完,就直接从这个SegmentBuffer取一个id返回。否则就将当前SegmentBuffer与备用SegmentBuffer进行交换,然后从交换后的SegmentBuffer取一个id返回给业务系统。

右边是备用SegementBuffer的更新流程:

ID生成系统会判断内存中当前Segement使用率是否达到了10%

Leaf框架的优化点之一就是使用了双SegmentBuffer进行优化,如果单个SegmentBuffer,在id消耗完之后,ID生成系统需要从数据库获取新的号段,填充到SegmentBuffer,然后供业务系统消费,此时业务系统发来的请求需要进行等待。如果是双SegmentBuffer,可以在一个SegmentBuffer的id使用率达到阀值(默认是10%),就触发另一个SegmentBuffer的异步更新,提取把号段获取到,存储到内存中。

百度的uid-generator

默认模式

每次启动时向数据库插入一条数据,这行数据的主键是自增的,主键id就是workId,

因为默认是snowflake算法是1标志位+41位时间戳+10位机器号+12位序列号,

因为百度的是每次启动都获取新的机器号,所以它修改了这些位数配比,是

1标志位+28位的时间差+22位的机器号+13位的序列号,所以总共支出2的22次方次启动,也就是400万次启动。

解决时间回拨问题:

  • 启动时时间回拨

因为是每次都用新的机器号,所以当前机器号都是之前没有的,所以即便时间戳回拨也不影响。

  • 运行时时间回拨

会使用lastSecond来记录上次生成id的时间戳,如果当前时间戳比lastSecond还小,就抛出异常。

缓存模式

主要继承自默认模式,只是用一个环形数组来存储生成好的id,每次去环形数组中去,默认大小是2的13次方,8192。这种模式使用的时间取得不是实时的系统时间,而且使用启动时的时间,每次生成一组id时,对之前保存的时间+1。

阀值检测,然后填充

取id时,可用id数小于阀值50%时,去填充

定期填充

会去检查环形数组中id使用情况,然后生成一组最大序列号个数的id(默认是8192个),然后进行填充,多的直接丢弃掉,

美团的leaf

snowflake模式

1位的符号位+41位的时间戳+10位的workID+10位的序列号

1 时钟回拨问题

若机器出现时钟回拨,会产生重复id

1)启动时回拨

启动时获取其他机器的时间,计算出平均时间后,将本机时间与平均时间比较,若超过阈值则启动失败。

2)运行时回拨
  1. 直接拒绝,抛异常
  2. 若回拨时间小于阈值,则睡眠
  3. 若回拨时间大于阈值,直接拒绝服务并报错,或者更换机器号,或者利用拓展位
时间类型
  • 时间戳:自1970.01.01起(今日头条)
  • 时间差:自选起始时间(百度、美团)
2)时间精度
  • 秒级:支持峰值更高(今日头条、百度)
  • 毫秒级:记录时间粒度更细(美团)

汽车之家是自增的id,只是作品类型不同,文章,视频等

头条,百度,美团也是使用了snowFlake只是改变了位数占比。

比如头条是去掉了符号标示位,缩短了时间戳,前31位是秒级时间戳+自定义的序列(可能是机器id+递增的数),记录的时间粒度会粗一些,每一秒生成的id会少一些,但是支持的峰值会高一些,时间戳也可以不从1970年开始选,也可以从自定义的时间开始选,这样支持的年长会多一些。

3 机器号

纯机器号,或,机房号+机器号

1)物理机
  • 手动配置
  • zookeeper(美团) 在zookeeper如未注册则创建持久顺序节点,顺序号当机器号,并本地缓存
2)虚拟机
  • redis 利用redis原子计数器,虚拟机启动后,请求计数器,按机器号位数取余
  • zookeeper 从0到机器号上限,在zookeeper尝试创建临时节点,成功则为当前机器号
  • 数据库(百度) 利用系统变量的host和port为唯一索引,在数据库中存取,主键作为机器id

参考链接:

https://tech.meituan.com/2017/04/21/mt-leaf.html

缺点是

1.趋势递增,容易被猜到订单量,第一条下单多少,第二条下单多少,可以自己加随机数。

2.依赖机器时间,如果机器时间出现回拨,变成以前的时间,可能会导致id重复。

解决回拨的问题

就是机器需要同步时间,不一致会对时间回拨,可能会导致id重复,像百度的snowflake是几台特定的服务器,每次的机器id也是从数据库里面生成的,所以不需要做时间同步。

3 类snowflake算法

采用snowflake算法思想,类snowflake算法根据各自业务需要,做出了不同改动。比如改变了位数占比,添加了业务线等其他信息

厂商 长度 组合 说明
美团Leaf 64位 1+41+10+12 沿用snowflake方案的bit位设计,1符号位+41位毫秒时间戳+10位机器位+12位序列号,只不过41位毫秒时间戳存的是某个指定时间点后的时间差
百度UidGenerator 64位 1+28+22+13 1符号位+28秒级时间差(最多可支持约8.7年)+22位机器号(最多可支持约420w次机器启动)+13位序列号
今日头条-文章 64位 1+31+32 1符号位+31秒级时间戳+32位其他
今日头条-微头条 52位 1+31+20 1符号位+31秒级时间戳+20位其他
我们自己的方案 54位 1+30+10+13 其实跟百度的方案类似,只是调小了机器位分配,增大了时间位,因为百度值支持8.7年,我们的可以支持34年生成出来的结果,转换10进制是最小14位,目前最大id是15位(因为目前时间距离自定义的时间点比较近)
附:位数对应
二进制 45-47位 48-50位 51-54位 55-57位 58-60位 61-64位
十进制 14位 15位 16位 17位 18位 19位

调研了Leaf和uid-generator,由于Leaf有额外的zookeeper依赖,

所以选用了uid-generator,做的改进如下:

1.原本是一旦时钟回拨就抛出异常,修改为时钟回拨小于1s时,就不抛出抛出异常,进行等待1s。

2.加了序列号抛弃策略。按照原有序列号位数分配,是13位,就是每秒可以生成的id数是8192个id,如果并发量比较小,由于每秒获取的id的序列号部分都是从0开始的,或导致后缀0的数据会比较多,容易造成数据倾斜的问题,而且也容易泄露数据信息。可以增加抛弃策略,就是取每一秒的id时,计算一个最大值为序列号的10%随机数,从这个随机数开始取。

3.修改了位数分配,原有的时间位是28位的秒级时间差,最长服务年限只支持8.7年,我们把时间差位数分配了30位,最长可以支持34年。原本机器位是22位,支持启动400万次,我们其实不需要那么多次启动,调整成20位,支持100万次启动。1+30+20+13

4.增加机器位用完时的取余操作,便于复用。

leaf的缺点:

1.号段模式的信息安全性问题,不考虑机器线下和重启丢掉的这些id,id是完全连续的,容易被竞争对手猜到信息安全性。

2.小问题修复,就是Leaf为了减少对Zookeeper的依赖,在本地也存了一个上次使用的workid的缓存文件,保证在Zookeeper挂掉的情况下,id生成服务能正常启动,读取本地缓存的workid,然后启动。但是本地缓存文件里面只存储了workid,没有存储上次生成的id的时间戳,假如启动前服务器的时间被修改了,那么启动时就没法对时间进行校验,就会导致生成的id重复。

3.缺乏对最大支持年限的检查,时间戳部分溢出会影响符号位,导致生成的id是负数。

4.注册中心只支持Zookeeper,issue里面很多人提出,对于他们的项目来说,由于需要引入Zookeeper依赖,增加部署和维护Zookeeper成本。所以我fork了这个项目,增加了使用MySQL作为注册中心,以及本地配置作为注册中心的模块。

相关实践学习
基于MSE实现微服务的全链路灰度
通过本场景的实验操作,您将了解并实现在线业务的微服务全链路灰度能力。
相关文章
|
3月前
|
缓存 算法 NoSQL
【分布式详解】一致性算法、全局唯一ID、分布式锁、分布式事务、 分布式缓存、分布式任务、分布式会话
分布式系统通过副本控制协议,使得从系统外部读取系统内部各个副本的数据在一定的约束条件下相同,称之为副本一致性(consistency)。副本一致性是针对分布式系统而言的,不是针对某一个副本而言。强一致性(strong consistency):任何时刻任何用户或节点都可以读到最近一次成功更新的副本数据。强一致性是程度最高的一致性要求,也是实践中最难以实现的一致性。单调一致性(monotonic consistency):任何时刻,任何用户一旦读到某个数据在某次更新后的值,这个用户不会再读到比这个值更旧的值。
401 0
|
8天前
|
存储 SQL 算法
搞定了 6 种分布式ID,分库分表哪个适合做主键?
在《ShardingSphere5.x分库分表原理与实战》系列的第七篇文章中,作者探讨了分布式ID在分库分表中的重要性,以及如何利用`ShardingSphere-jdbc`的多种主键生成策略。文章介绍了`UUID`、`NanoID`、自定义雪花算法和`CosId`等策略的优缺点,并警告不要在SQL中手动拼接主键字段。此外,文章还展示了如何配置这些策略,并提醒读者`CosId`在5.2.0版本可能不可用。最后,文章讨论了如何自定义分布式主键生成算法,并强调选择策略时要考虑全局唯一性、性能和易用性。
|
27天前
|
缓存 算法 关系型数据库
深度思考:雪花算法snowflake分布式id生成原理详解
雪花算法snowflake是一种优秀的分布式ID生成方案,其优点突出:它能生成全局唯一且递增的ID,确保了数据的一致性和准确性;同时,该算法灵活性强,可自定义各部分bit位,满足不同业务场景的需求;此外,雪花算法生成ID的速度快,效率高,能有效应对高并发场景,是分布式系统中不可或缺的组件。
深度思考:雪花算法snowflake分布式id生成原理详解
|
1月前
|
算法 Java 数据中心
分布式ID生成系统之雪花算法详解
在当今的云计算和微服务架构盛行的时代,分布式系统已成为软件开发的重要组成部分。随着系统规模的扩大和业务的复杂化,对数据一致性和唯一性的要求也越来越高,尤其是在全局唯一标识符(ID)的生成上。因此,分布式ID生成系统应运而生,成为保证数据唯一性和提高系统可扩展性的关键技术之一。雪花算法(Snowflake)是Twitter开源的一种算法,用于生成64位的全局唯一ID,非常适用于分布式系统中生成唯一标识符。下面我们将深入探讨雪花算法的原理、结构和实现方式。
98 2
 分布式ID生成系统之雪花算法详解
|
1月前
|
NoSQL 算法 Java
【Redis】4、全局唯一 ID生成、单机(非分布式)情况下的秒杀和一人一单
【Redis】4、全局唯一 ID生成、单机(非分布式)情况下的秒杀和一人一单
65 0
|
2月前
|
存储 算法 NoSQL
全网最全的分布式ID生成方案解析
全网最全的分布式ID生成方案解析
102 0
|
3月前
|
算法 NoSQL 关系型数据库
9种 分布式ID生成方式
9种 分布式ID生成方式
415 0
|
4月前
|
SQL Java 关系型数据库
Springcloud实战之自研分布式id生成器7
Springcloud实战之自研分布式id生成器7
Springcloud实战之自研分布式id生成器6
Springcloud实战之自研分布式id生成器6
|
4月前
|
Java Maven
Springcloud实战之自研分布式id生成器5
Springcloud实战之自研分布式id生成器5
Springcloud实战之自研分布式id生成器5

热门文章

最新文章