分布式系统 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