分布式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也同样可以实现,原理就是利用redis的 incr命令实现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有两种持久化方式RDB和AOF
RDB会定时打一个快照进行持久化,假如连续自增但redis没及时持久化,而这会Redis挂掉了,重启Redis后会出现ID重复的情况。AOF会对每条写命令进行持久化,即使Redis挂掉了也不会出现ID重复的情况,但由于incr命令的特殊性,会导致Redis重启恢复的数据时间过长。
类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
根据这个算法的逻辑,只需要将这个算法用Java语言实现出来,封装为一个工具方法,那么各个业务应用可以直接使用该工具方法来获取分布式ID,只需保证每个业务应用有自己的工作机器id即可,而不需要单独去搭建一个获取分布式ID的应用。
实现
特点
- 本地计算,发号服务不会成为性能瓶颈
- 强依赖时钟,对时间的要求比较敏感,需要中心服务提供节点保活,时钟校验等服务
- 满足信息安全的需求
基于业务特点的设计
业界方案
Leaf
号段模式
在数据号段模式的基础上,对号段适用完毕的情况和容灾方案进行优化
双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的生成策略。
