什么是分布式 ID
分布式 ID 是指,在分布式环境下可用于对数据进行标识且易存储的全局唯一的 ID 标识。
为什么需要分布式 ID
对于单体系统来说,主键ID可能会常用主键自动的方式进行设置,这种ID生成方法在单体项目是可行的。
对于分布式系统,分库分表之后,就不适应了,比如订单表数据量太大了,分成了多个库,如果还采用数据库主键自增的方式,就会出现在不同库或表中id一致的情况。
分布式 ID 需要满足的条件
分布式 ID 是我们在非常多的场景下用到的组件,对其要求比较高,其一般需要满足以下条件:
- 全局唯一性:ID是作为唯一的标识,不能出现重复
- 高性能:高可用低延时,ID 生成速度要快,否则反倒会成为业务瓶颈
- 高可用:尽量保证服务的可用性,多实例化,避免因一个实例挂掉影响整个业务应用的运行
- 容易接入:要秉着拿来即用的设计原则,在系统设计和实现上要尽可能的简单,避免增加开发人员的使用成本
- 趋势递增:互联网比较喜欢MySQL数据库,而MySQL数据库默认使用InnoDB存储引擎,其使用的是聚集索引,使用有序的主键ID有利于保证写入的效率
- 单调递增:保证下一个ID大于上一个ID,这种情况可以保证事务版本号,排序等特殊需求实现
- 信息安全:前面说了ID要递增,但是最好不要连续,如果ID是连续的,容易被恶意爬取数据,指定一系列连续的,所以ID递增但是不规则是最好的
常用分布式 ID 生成方案
下面列出的这几种方案都是生成 ID 的常用方法:
- 使用 UUID 生成 ID
- 使用数据库自增生成 ID
- 使用数据库号段模式生成 ID
- 使用 Redis 实现生成 ID
- 使用 Zookeeper 生成 ID
- 根据雪花算法(Snowflake)算法生成 ID
- 百度Uidgenerator
- 美团Leaf
- 滴滴TinyID
使用 UUID 生成 ID
UUID 是通用唯一识别码的缩写(Universally Unique Identifier)。UUID 一般是由一组 32 位数的 16 进制数字所构成,以连字号分为五段,形式为8-4-4-4-12的36个字符,常包含时间戳和 MAC 地址信息这些元素,标准的 UUID 格式为:
xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
优点
- 高性能
- 实现简单
- 不需要第数据库等第三方组件依赖
缺点
- 并不是趋势递增,不方便排序
- 生成的 ID 只能用字符串类型存储,占用空间大
- 生成的串没有规律,出问题时不易根据 ID 进行排查
使用数据库自增生成 ID
在分布式系统中我们可以多部署几台机器,每台机器设置不同的初始值,且步长和机器数相等。比如有两台机器:设置步长step为2,Server1的初始值为1(1,3,5,7,9,11…)、Server2的初始值为2(2,4,6,8,10…)。
这种方案看起来是可行的,但是如果要扩容,步长step等要重新设置,假如只有一台机器,步长就是1,比如1,2,3,4,5,6,这时候如果要进行扩容,就要重新设置,机器2可以挑一个偶数的数字,这个数字在扩容时间内,数据库自增要达不到这个数的,然后步长就是2,机器1要重新设置step为2,然后还是以一个奇数开始进行自增。这个过程看起来不是很杂,但是,如果机器很多的话,那就要花很多时间去维护重新设置。
优点
- 实现简单
- 趋势递增
缺点
- ID没有了单调递增的特性,只能趋势递增,有些业务场景可能不符合
- 数据库压力还是比较大,每次获取ID都需要读取数据库,只能通过多台机器提高稳定性和性能
- 水平扩展比较麻烦,需要手动调整集群数据库中的初始值与步长
使用数据库号段模式生成 ID
这种模式也是现在生成分布式ID的一种方法,在使用号码模式时,我们通常会先建立一张表用于记录上述的 ID 号段范围,一般表内容如下:
CREATE TABLE id_generator (
id int(10) NOT NULL AUTO_INCREMENT,
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`)
)
每次从数据库中获取号段 ID 的范围时,都会执行更新语句,其中计算新号段范围最大值 max_id 的公式是 max_id + step 组成,所以 SQL 中设置 max_id = max_id+step 来执行更新语句,更新数据库中这个范围最大值 max_id,然后再通过查询语句查询更新后 ID 最大值,再根据最大值 max_id 与步长 step 计算出待生成的 ID 的范围,其中操作的 SQL 如下:
update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = #{biz_type};
SELECT `max_id`, `step`, `version` FROM `myid` WHERE `biz_type` = #{biz_type};
优点
- 使用缓存机制,容灾性高,即使数据库不可用还能撑一段时间。
- 可以自定义每次扩展的大小,控制 ID 生成速度;
- 可以设置生成 ID 的初始范围,方便业务从原有的 ID 方式上迁移过来。
- 有比较成熟的方案,像百度Uidgenerator,美团Leaf
缺点
- 依赖于数据库实现,数据库宕机会造成整个系统不可用。
- ID 号码不够随机,可能够泄露发号数量的信息,不太安全。
使用 Redis 实现生成 ID
Redis分布式ID实现主要是通过提供像 INCR 和 INCRBY 这样的自增原子命令,由于Redis单线程的特点,可以保证ID的唯一性和有序性。
优点
- 实现简单;
- 有序递增,方便排序;
缺点
- 强依赖于 Redis,可能存在单点问题;
- 如果 Redis 超时,可能会对业务造成影响;
- 占用宽带,而且需要考虑网络延时等问题带来地性能冲击。
使用 Zookeeper 生成 ID
在 Zookeeper 中主要通过节点数据版本号来生成序列号,可以生成 32 位和 64 位的数据版本号,客户端可以使用这个版本号来作为唯一的序列号。在 Zookeeper 中本身就是支持集群模式,所以能保证高可用性,且生成的 ID 为趋势递增且有序,不过在实际使用中很少用 Zookeeper 来充当 ID 生成器,因为 Zookeeper 中存在强一致性,在高并发场景下其性能可能很难满足需求。
不过由于使用 Zookeeper 节点的版本号来充当 ID 号是比较繁琐,需要创建节点获取生成的 ID,然后去掉节点命令前缀,只截取数字部分,最后还要异步执行删除节点(启动新的线程执行删除节点操作,防止占用生成ID线程执行的实际)。过程比较耗时且繁琐,所以,在操作 Zookeeper 时经经常不会采用该方案,常使用 Curator 客户端提供的基于乐观锁的计数器来自增实现 ID 生成,这个过程和数据库自增生成 ID 类似。
优点
- 高可用
- 趋势递增
缺点
- 性能差
- 定期删除之前生成的节点,比较繁琐
根据雪花算法(Snowflake)算法生成 ID
Snowflake,雪花算法是由 Twitter 开源的分布式ID生成算法,以划分命名空间的方式将
64-bit位分割成多个部分,每个部分代表不同的含义,64位,在java中Long类型是64位的,所以java程序中一般使用Long类型存储
其结构组成:
- 第一部分:第一位占用1bit,始终是0,是一个符号位,不使用
- 第二部分:第2位开始的41位是时间戳。41-bit位可表示241个数,每个数代表毫秒,那么雪花算法可用的时间年限是(241)/(1000606024365)=69 年的时间
- 第三部分:10-bit位可表示机器数,即2^10 = 1024台机器。通常不会部署这么多台机器
- 第四部分:12-bit位是自增序列,可表示2^12 = 4096个数。觉得一毫秒个数不够用也可以调大点
优点:
- 高性能
- 趋势递增
- 可以灵活调整结构
- 不需要第数据库等第三方组件依赖
缺点:
- 强依赖时钟,可能发生时钟回拨导致生成的 ID 重复
百度 Uidgenerator
百度的 UidGenerator 是百度开源基于Java语言实现的唯一ID生成器,是在雪花算法 snowflake 的基础上做了一些改进。
详细介绍请看官网。
美团 Leaf
Leaf 提供两种生成的ID的方式:号段模式(Leaf-segment)和 snowflake 模式(Leaf-snowflake)。你可以同时开启两种方式,也可以指定开启某种方式,默认两种方式为关闭状态。
详细介绍请看官网。
滴滴 TinyID
Tinyid 是用Java开发的一款分布式id生成系统,基于数据库号段算法实现。Tinyid 扩展了 leaf-segment 算法,支持了多数据库和 tinyid-client。
详细介绍请看官网。
参考
SmileNicky的博客
myf008的博客