0
点赞
收藏
分享

微信扫一扫

分布式系统(唯一) ID 生成器实现方案


分布式系统 ID

一个唯一 ID 在一个分布式系统中是非常重要的一个业务属性,其中包括一些如订单 ID,消息 ID ,会话 ID,他们都有一些共有的特性:

  • 全局唯一(唯一标识某个请求,某个业务)
  • 趋势递增

 

解决方案

基于数据库

可以利用 MySQL 中的自增属性 auto_increment 来生成全局唯一 ID,也能保证趋势递增。 但这种方式太依赖 DB,如果数据库挂了那就非常容易出问题

 

水平扩展改进

可以将数据库水平拆分,如果拆为了两个库 A 库和 B 库。 A 库的递增方式可以是 0 ,2 ,4 ,6。B 库则是 1 ,3 ,5 ,7。这样的方式可以提高系统可用性,并且 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;

存在的问题:

  • 想要扩容增加性能变的困难,之前已经定义好了 A、B 库递增的步数,新加的数据库不好加入进来,水平扩展困难
  • 也是强依赖与数据库,并且如果其中一台挂掉了那就不是绝对递增了

 

号段模式

号段模式可以理解成从数据库批量获取ID。将ID缓存在本地,提升效率。

比如每次从数据库获取ID时,就获取一个号段,如(1,1000],这个范围表示1000个ID,业务应用在请求提供ID时,只需要在本地从1开始自增并返回,而不需要每次去请求数据库,一直到本地自增到1000时,也就是当前号段已经用完了,才去数据库重新获取下一号段。

CREATE TABLE `id_generator` (
`id` int(10) NOT NULL AUTO_INCREMENT COMMENT '主键id',
`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 '版本号',
`desc` varchar(255) DEFAULT NULL COMMENT '业务类型描述',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

+----+--------+------+----------+---------+----------------+
| id | max_id | step | biz_type | version | desc |
+----+--------+------+----------+---------+----------------+
| 1 | 1 | 1000 | 101 | 0 | 用户id生成规则 |
+----+--------+------+----------+---------+----------------+

等这批号段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 biz_type = #{bizType} and version = #{version}

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

 

号段模式改进-双buffer方案

在高并发场景下, id用的很快, 如果此时有100个服务在某一个时刻本地缓存的id都用完了, 同时去请求[ID服务]。 因为竞争问题, 所以只有一个服务操作数据库, 其他的都会被阻塞。 出现的现象就是一会儿突然系统耗时变长,一会儿好了,就是这个原因导致的,怎么去解决?

双buffer方案具体流程:

  • 当前获取ID在buffer1中,每次获取ID在buffer1中获取;
  • 当buffer1中的Id已经使用到了100,也就是达到区间的10%;
  • 达到了10%,先判断buffer2中有没有去获取过,如果没有就立即发起请求获取ID线程,此线程把获取到的ID,设置到buffer2中;
  • 如果buffer1用完了,会自动切换到buffer2;
  • buffer2用到10%了,也会启动线程再次获取,设置到buffer1中;
  • 依次往返。

双buffer的方案,小伙伴们有没有感觉很酷,这样就达到了业务场景用的ID,都是在jvm内存中获得的,从此不需要到数据库中获取了。允许数据库宕机时间更长了。

因为会有一个线程,会观察什么时候去自动获取。两个buffer之间自行切换使用。就解决了突发阻塞的问题。

 

本地 UUID 生成

还可以采用 UUID 的方式生成唯一 ID,由于是在本地生成没有了网络之类的消耗,所有效率非常高

存在的问题:

  • 生成的 ID 是无序性的,不能做到趋势递增
  • 由于是字符串并且不是递增,所以不太适合用作主键

 

采用本地时间

这种做法非常简单,可以利用本地的毫秒数加上一些业务 ID 来生成唯一ID,这样可以做到趋势递增,并且是在本地生成效率也很高

存在的问题:

  • 当并发量足够高的时候唯一性就不能保证了

 

Redis 生成 ID

依赖于 Redis 是单线程的,所以可以用来生成全局唯一的 ID。 可以使用 Redis 的原子性操作 INCR 和 INCRBY 来实现。

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

可以使用 Redis 集群方案来获取更高的吞吐量。

假如一个 Redis Cluster 中有5台 Redis 节点,可以初始化每个 Redis 节点的值分别为1,2,3,4,5, 然后步长都是5, 各个 Redis 节点生成的 ID为:

  • node_1: 1, 6, 11, 16, 21 …
  • node_2: 2, 7, 12, 17, 22 …
  • node_3: 3, 8, 13, 18, 23 …
  • node_4: 4, 9, 14, 19, 24 …
  • node_5: 5, 10, 15, 20, 25 …

优点:

  • 不依赖于数据库,灵活方便,且性能优于数据库
  • 数字ID天然排序,对分页或者需要排序的结果很有帮助。

缺点:

  • 果系统中没有Redis,还需要引入新的组件,增加系统复杂度
  • 需要编码和配置的工作量比较大

 

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

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

 

Twitter 雪花算法

​​Twitter的分布式自增ID算法snowflake (Java版)​​

 

Reference

  • ​​你想了解一线大厂的分布式唯一ID生成方案吗?​​
  • ​​分享大厂分布式唯一ID设计方案,快来围观​​


举报

相关推荐

0 条评论