基于号段模式的分布式ID设计

这里介绍基于号段模式的分布式ID设计思路、方案

abstract.png

背景

对于分布式ID生成方案,很朴素的思路就是利用DB的自增主键进行实现。但其存在明显的弊端。业务系统每次获取ID都需要请求、访问数据库。数据库压力较大。如果这里的数据库是单机,则容易发展成为整个系统的单点故障。一旦该DB宕机,则整个业务系统都将无法正常继续

而如果这里的数据库是采用集群的形式。比如这里有两个DB——#1实例、#2实例。其中,#1实例自增ID的起始值、步长分别为1、2;#2实例自增ID的起始值、步长分别为2、2。则#1实例生成的ID序列则是1、3、5、7、···,#2实例生成的ID序列则是2、4、6、8、···。然后再提供一个发号服务来对外提供服务即可。业务系统只需请求发号服务获取ID,而发号服务内部则会随机选取一个数据库实例来获取相应的数据库自增ID。虽然采用集群的方式进一步地提高了系统的可用性、解决了单点故障。但缺陷依然很明显,拓展性非常差。目前DB集群中存在2台实例,如果后期再增加1台变成含3个实例的集群。此时,我们需要人工手动修改原#1、#2实例的步长。而且还需要将#3实例的起始值设置的足够长,以避免发生ID重复的问题。甚至说在整个过程中,发号服务需要对外停止服务才能完成扩容

号段模式

所谓号段模式,则是指发号服务每次会从数据库获取一批ID,然后将这批ID缓存到发号服务本地。业务系统每次向发号服务请求ID时,后者会先判断其本地是否还有可用的ID分配给业务系统。如果有则直接分配,反之则再次访问数据库来批量获取ID。显然在号段模式下,由于发号服务不用每次都请求数据库。一方面减轻了数据库的压力,另一方面提高了系统的可用性、可靠性。同时,后续由于业务拓展、业务系统激增时,对基于号段模式的设计方案进行分库分表也非常便于实现

具体地,当发号服务请求数据库时,即会获取一个号段范围内的ID,例如[1,2000]号段表示1~2000的ID。发号服务会把这2000个ID缓存到本地。直到该业务第2001次向发号服务请求ID时,由于发号服务对于该业务的本地ID都已经被使用完毕了。故其会再次请求数据库,获取下一个号段范围的ID——即[2001,4000]号段

号段模式下的数据表,典型设计如下所示。其中

  • service_name:用于实现服务隔离
  • business_name:用于实现服务下不同业务场景的隔离
  • current_max_id:则表示该业务当前已经分配了的最大ID值
  • step:表示发号服务每次批量获取ID的数量。换言之,每次更新数据库current_max_id的增量值即是step值
  • version:乐观锁,保证并发场景下的可靠性
1
2
3
4
5
6
7
8
9
10
11
12
-- 基于号段模式的分布式ID数据表
create table common_sequence (
`id` bigint(40) NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`service_name` varchar(255) DEFAULT NULL COMMENT '服务名称',
`business_name` varchar(255) NOT NULL COMMENT '服务的业务名称',
`current_max_id` bigint(40) NOT NULL COMMENT '当前最大ID值',
`step` int(4) NOT NULL COMMENT '步长',
`version` int(4) NOT NULL COMMENT '版本号',
`create_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`modify_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
primary key (`id`)
) comment = '分布式ID数据表';

发号服务每次请求数据库获取号段时的SQL语句大体如下所示

1
2
3
4
5
6
7
# 发号服务获取号段时执行的SQL
update common_sequence
set current_max_id = current_max_id + step,
version = version + 1
where service_name = #{service_name}
and business_name= #{business_name}
and version = #{version}
0%