九种分布式ID生成算法详解

一、分布式ID简介

1、什么是分布式ID?

在我们业务数据量不大的时候,单库单表完全可以支撑现有业务,数据再大一点搞个MySQL主从同步读写分离也能对付。

但随着数据日渐增长,主从同步也扛不住了,就需要对数据库进行分库分表,但分库分表后需要有一个唯一ID来标识一条数据,数据库的自增ID显然不能满足需求;例如我们的订单,需要有一个全局唯一标识的订单号,这个订单号就是分布式ID

2、分布式ID需要满足那些条件?

    1. 全局唯一:必须保证ID是全局性唯一的,基本要求
    1. 高可用:
    1. 高性能:高可用低延时,ID生成响应要块,否则反倒会成为业务瓶颈
    1. 简单通用:拿来即用的原则

二、 分布式ID都有哪些生成方式?

  1. UUID
  1. 数据库自增ID
  1. 数据库多主模式
  1. 号段模式
  1. Redis
  1. 雪花算法(SnowFlake)
  1. 滴滴出品(TinyID)
  1. 百度 (Uidgenerator)
  1. 美团(Leaf)
  1. MongoDB的ObjectID

1、UUID

   ​String uuid = UUID.randomUUID().toString().replaceAll("-","");​

优点:

  • 足够简单,只需要上面一行代码即可生成一个全局唯一的ID,

缺点:

  • 因为UUID是随机的,没有具体的业务含义,如果用UUID作为订单号,是没有意义的,从这个UUID看不到跟订单有任何的关联关系,
  • UUID是一个无序的字符串,不具备趋势自增特性
  • 长度过长,存储以及查询对MySQL的性能消耗较大,如果用来作为主键,索引的性能会很差

2、数据库自增ID

基于数据库的​auto_increment​自增ID也可以生成一个分布式ID

优点:

  • 实现简单,ID单调自增,数值类型查询速度快

缺点:

  • 访问量激增时MySQL本身就是系统的瓶颈,用它来实现分布式服务风险比较大,不推荐!

3、数据库集群模式

为了解决第二种数据库单个DB的风险,可以采用数据库集群的模式来解决这个问题。

  • 可以使用主从模式解决单DB不满足高可用的问题,需要考虑的问题是:主从之间同步是存现时间延迟的,如果主DB挂了可能会有重复的ID生成(原因是什么? --  主从复制延时导致)
  • 如果担心一个主节点挂掉没法用,那就做双主模式集群,也就是两个Mysql实例都能单独的生产自增ID。需要考虑的问题是:1:两个MySQL实例的自增ID都从1开始,会生成重复的ID,怎么解决?2:后续DB扩容怎么办

4、基于数据库的号段模式

号段模式是当下分布式ID生成器的主流实现方式之一,号段模式可以理解为从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将本号段,生成1~1000的自增ID并加载到内存,典型的是TinyID。表结构如下:

​CREATE TABLE id_generator (​

​  id int(10) NOT NULL,​

​  max_id bigint(20) NOT NULL COMMENT '当前最大id',​

​  step int(20) NOT NULL COMMENT '号段的布长',​

​  biz_type    int(20) NOT NULL COMMENT '业务类型',​

​  version int(20) NOT NULL COMMENT '版本号',​

​  PRIMARY KEY (`id`)​

​) ​

等这批号段ID用完,再次向数据库申请新号段,对​max_id​字段做一次​update​操作,​update max_id= max_id + step​,update成功则说明新号段获取成功,新的号段范围是​(max_id ,max_id +step]​。

SQL语句:

​update id_generator set max_id = #{max_id+step} where biz_type = XXX​

为了解决并发更新的问题,使用乐观锁控制,优化后的SQL:

​update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX​

优点:

  • 这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多

5、基于Redis模式

Redis也同样可以实现,原理就是利用redis的 incr命令实现ID的原子性自增。

Redis是内存数据库,需要考虑持久化的问题,redis有两种持久化方式RDB和AOF

  • RDB:会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。
  • AOF:对每条写命令进行持久化,即使redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。

6、基于雪花算法(Snowflake)模式

雪花算法(Snowflake)是twitter公司内部分布式项目采用的ID生成算法,开源后广受国内大厂的好评,在该算法影响下各大公司相继开发出各具特色的分布式生成器。
 

Snowflake生成的是Long类型的ID,一个Long类型占8个字节,每个字节占8比特,也就是说一个Long类型占64个比特。

Snowflake ID组成结构:​正数位​(占1比特)+ ​时间戳​(占41比特)+ ​机器ID​(占5比特)+ ​数据中心​(占5比特)+ ​自增值​(占12比特),总共64比特组成的一个Long类型。

  • 第一个bit位(1bit):Java中long的最高位是符号位代表正负,正数是0,负数是1,一般生成ID都为正数,所以默认为0。
  • 时间戳部分(41bit):毫秒级的时间,不建议存当前时间戳,而是用(当前时间戳 - 固定开始时间戳)的差值,可以使产生的ID从更小的值开始;41位的时间戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工作机器id(10bit):也被叫做​workId​,这个可以灵活配置,机房或者机器号组合都可以。
  • 序列号部分(12bit),自增值支持同一毫秒内同一个节点可以生成4096个ID

Snowflake雪花算法最核心的是中间的10位工作机器ID的分配,做到自动生成workID,避免运维人员的去分配

优点:

  • 毫秒数在高位,自增序列在低位,整个ID都是趋势递增的。
  • 不依赖数据库等第三方系统,以服务的方式部署,稳定性更高,生成ID的性能也是非常高的。
  • 可以根据自身业务特性分配bit位,非常灵活。

缺点:

  • 强依赖机器时钟,如果机器上时钟回拨,会导致发号重复或者服务会处于不可用状态。

根据算法的思路,使用java语言可以直接生成一个工具类,进行本地调用用来生成全局唯一的ID,

参考github地址:https://github.com/beyondfengyu/SnowFlake

7、滴滴出品(TinyID)

​Tinyid​由滴滴开发,Github地址:https://github.com/didi/tinyid

​Tinyid​是基于号段模式每个服务获取一个号段(1000,2000]、(2000,3000]、(3000,4000]

1:具体实现原理:

  • tinyid是基于数据库发号算法实现的,简单来说是数据库中保存了可用的id号段,tinyid会将可用号段加载到内存中,之后生成id会直接内存中产生。
  • 可用号段在第一次获取id时加载,如当前号段使用达到一定量时,会异步加载下一可用号段,保证内存中始终有可用号段。当前号段使用完毕,下一号段会替换为当前号段。依次类推。

2:架构图

提供了两种接入方式:

  1. http接入,通过访问tinyid-server来生成ID,其中tinyid-server推荐部署到多个机房的多台机器,这种方式需要考虑网络的延时性
  1. 使用tinyid-client来获取id,

优点:

  • id为本地生成(调用AtomicLong.addAndGet方法),性能大大增加
  • client对server访问变的低频,减轻了server的压力,无须担心网络延迟
  • 即便所有server挂掉,因为client预加载了号段,依然可以继续使用一段时间 

缺点:

  • 如果client机器较多频繁重启,可能会浪费较多的id(原因是什么?

8、百度 (Uidgenerator)

UidGenerator是Java实现的, 基于Snowflake算法的唯一ID生成器。Github地址:https://github.com/baidu/uid-generator

UidGenerator以组件形式工作在应用项目中, 支持自定义workerId位数和初始化策略, 从而适用于docker等虚拟化环境下实例自动重启、漂移等场景, UidGenerator通过借用未来时间来解决sequence天然存在的并发限制; 采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费。

具体算法实现:

Snowflake算法:

Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。默认采用上图字节分配方式:

  • sign(1bit)
  • 固定1bit符号标识,即生成的UID为正数。
  • delta seconds (28 bits)
  • 当前时间,相对于时间基点"2016-05-20"的增量值,单位:秒,最多可支持约8.7年
  • worker id (22 bits)
  • 机器id,最多可支持约420w次机器启动。内置实现为在启动时由数据库分配,默认分配策略为用后即弃,后续可提供复用策略。
  • sequence (13 bits)
  • 每秒下的并发序列,13 bits可支持每秒8192个并发。

9、美团(Leaf)

Leaf由美团开发,github地址:https://github.com/Meituan-Dianping/Leaf

同时支持号段模式和Snowflake算法模式,可以切换使用。

1:Leaf-segment号段模式:

在使用数据库的方案上,做了如下改变: 

  1. 原方案每次获取ID都得读写一次数据库,造成数据库压力大。改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值。用完之后再去数据库获取新的号段,可以大大的减轻数据库的压力
  2. 各个业务不同的发号需求用biz_tag字段来区分,每个biz-tag的ID获取相互隔离,互不影响

号段模式的具体实现思路同TinyID一样

2:Leaf-snowflake雪花算法:

完全沿用snowflake方案的bit位设计,即是“1+41+10+12”的方式组装ID号。为了解决集权下workID的问题,美团的Leaf-snowflake跟百度不一样,百度是通过数据库来生成workID,而美团是通过Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerID,所以Leaf是依赖ZK服务的。

特点:

  1. 弱依赖ZooKeeper:除了每次会去ZK拿数据以外,也会在本机文件系统上缓存一个workerID文件。当ZooKeeper出现问题,恰好机器出现问题需要重启时,能保证服务能够正常启动。这样做到了对ZK的弱依赖
  2. 解决时钟问题:因为这种方案依赖时间,如果机器的时钟发生了回拨,那么就会有可能生成重复的ID号,需要解决时钟回退的问题。

猜你喜欢

转载自blog.csdn.net/Crystalqy/article/details/108410616