分布式ID生成方案

445 阅读7分钟

分布式ID生成方案

在日常的业务开发中,通常需要对一些数据做唯一标识,例如为大量抓取的文章入库时分配一个唯一的id,为用户下的订单分配订单号等等。并发量小的时候,通常会使用数据库自增的主键id作为唯一id。并发量大的时候就会考虑使用一些分布式ID的生成方案来生成id。

通常由于业务和性能的要求,可能会要求方案具有以下特性:

  • 全局自增,随机的ID可能导致会导致数据库的随机插入,也不能进行排序。
  • 单调递增:保证下一个ID一定大于上一个ID,例如事务版本号、IM增量消息、排序等特殊需求。
  • 具有一定非连续性,以便不能直接从ID推断出数据量
  • 全局唯一,出现重复会导致数据库错误
  • 去中心化,防止中心发号服务成为性能瓶颈

针对非连续的需要,可以在连续的方案上,增加ID丢弃策略作为补充

常见方案

UUID

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

实现

public static void main(String[] args){
  String uuid = UUID.randomUUID().toString();
  
}

特点

  • 满足唯一性
  • 本地算法计算,无外部依赖
  • 不满足自增
  • 可能存在ID太长,不是数字类型等问题

适用场景

临时的唯一性标识,如用户登录后的Session或token

单机数据库主键自增

利用数据库的auto_increment自增ID完全可以充当分布式ID

实现

CREATE DATABASE ‘SEQ_ID‘;
CREATE TABLE SEID.SEQUENCE_ID (
  id bigint(20) unsigned not null auto_increment,
  value char(10) not null default,
  PRIMARY KEY(id),
)ENGINE = InnoDB

当我们需要一个ID的时候,向表中插入一条记录返回主键ID

特点

  • 利用数据库主键特性,作为发号服务,发号服务可能成为系统瓶颈
  • 实现简单,具有自增特性,数据储存快
  • 单点DB存在崩溃风险
  • ID具有连续性

适用场景

并发量不高的业务

数据库集群发号

通过集群拓展单点DB的可用性,如果使用了多主架构,即有多个数据库实例可以进行发号,则需要手动设置不同实例的主键初始值和自增步长防止ID重复

实现

设置起始值自增步长

MySQL_1 配置:

set @@auto_increment_offset = 1;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

MySQL_2 配置:

set @@auto_increment_offset = 2;     -- 起始值
set @@auto_increment_increment = 2;  -- 步长

特点

  • 在数据库发号的基础上提高了效率和可用性
  • 可拓展性低下,一旦需要增加或减少数据库数量,需要手动调整每个数据库的初始值和步长
  • 不能保证单调递增

适用场景

数据库号段模式

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

实现

表结构如下:

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`)
) 

biz_type :代表不同业务类型

max_id :当前最大的可用id

step :代表号段的长度

version :是一个乐观锁,每次都更新version,保证并发时数据的正确性

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

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

由于多业务端可能同时操作,所以采用版本号version乐观锁方式更新,这种分布式ID生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多。

特点

  • 效率更高,一次申请一个号段,效率取决于step
  • 强依赖数据库
  • id连续,不能保证信息安全的需要
  • 高拓展性和可用性
  • 号段适用完毕还是会请求会数据库

基于Redis模式的发号服务

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

127.0.0.1:6379> set seq_id 1     // 初始化自增ID为1
OK
127.0.0.1:6379> incr seq_id      // 增加1,并返回递增后的数值
(integer) 2

redis实现需要注意一点,要考虑到redis持久化的问题。redis有两种持久化方式RDBAOF

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

类snowFlake算法

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

img图片源自网络,如有侵权联系删除

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

根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。

实现

github.com/beyondfengy…

特点

  • 本地计算,发号服务不会成为性能瓶颈
  • 强依赖时钟,对时间的要求比较敏感,需要中心服务提供节点保活,时钟校验等服务
  • 满足信息安全的需求

基于业务特点的设计

tech.meituan.com/2016/11/18/…

业界方案

Leaf

tech.meituan.com/2017/04/21/…

号段模式

在数据号段模式的基础上,对号段适用完毕的情况和容灾方案进行优化

双buffer优化

希望DB取号段的过程能够做到无阻塞,不需要在DB取号段的时候阻塞请求线程,即当号段消费到某个点时就异步的把下一个号段加载到内存中。而不需要等到号段用尽的时候才去更新号段。

方案

采用双buffer的方式,Leaf服务内部有两个号段缓存区segment。当前号段已下发10%时,如果下一个号段未更新,则另启一个更新线程去更新下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发,循环往复。

  • 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响。
  • 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新。
容灾

Master和Slave之间采用半同步方式[5] 同步数据。同时使用Atlas数据库中间件(已开源,改名为DBProxy)做主从切换。

snowFlake模式

使用ZK提供保活、时钟服务

uid-generator

uid-generator是由百度技术部开发,项目GitHub地址 github.com/baidu/uid-g…

uid-generator是基于Snowflake算法实现的,与原始的snowflake算法不同在于,uid-generator支持自定义时间戳工作机器ID序列号 等各部分的位数,而且uid-generator中采用用户自定义workId的生成策略。