文章目录
一、分布式id介绍
1、什么是分布式id
业务需求中需要对各种数据使用id进行唯一标识。
分布式ID是在分布式系统下使用的唯一标识,它用于保证数据的一致性和唯一性。
在传统的单机环境下,可以使用自增主键或UUID来生成唯一的ID,但在分布式环境下,由于多个节点同时生成ID,可能会出现重复的情况。
因此,需要一种能够生成全局唯一ID的解决方案,这就是分布式ID。
2、分布式id的特点
- 全局唯一:在分布式环境下无论是在哪个节点生成的ID,都必须是全局唯一的,不能出现重复的情况
- 趋势递增:生成的分布式ID通常要求能够近似有序递增,有助于后续业务需求使用id记性排序。ID 的有序性可以提升数据库写入速度,如在数据库中使用B+Tree数据结构时,自增顺序写入的数据能够保持B+Tree的结构稳定,从而提高存取效率
- 高可用性:分布式ID生成系统需要保证在任何时候都能正确生成ID,即使在某些节点故障的情况下,整个系统仍能保持正常运行
- 高性能性:在分布式环境下,特别是在高并发场景下,分布式ID生成系统需要能够迅速、高效地生成ID,且对服务本身的本地资源消耗小,以满足业务需求。
- 信息安全:分布式ID的生成过程需要保证安全,不能被恶意用户推测出下一个ID,以防止可能的攻击,比如获取订单号,获取下载链接等
二、UUid生成算法
1、JDK UUID
JDK提供了现成的UUID生成方式,如下代码所示,包含了32个16进制的数字,以-分为五段,8-4-4-4-12
UUID uuid = UUID.randomUUID();
//输出示例:5c89a67d-4541-42d1-85e9-807bd93f065b
System.out.println(uuid);
JDK的版本不同,生成UUID的规则也不同,实现方式参考文档:
从源码角度分析UUID的实现原理
- 版本 1 : UUID 是根据时间和节点 ID(通常是 MAC 地址)生成;
- 版本 2 : UUID 是根据标识符(通常是组或用户 ID)、时间和节点 ID 生成;
- 版本 3、版本 5 : 通过散列(hashing)名字空间(namespace)标识符和名称生成;
- 版本 4 : UUID 使用随机性open in new window或伪随机性open in new window生成
优点:生成速度快,简单易用,一行代码搞定,本地生成,没有网络损耗
缺点:
- 存储空间消耗大: UUID 是一个 128 位的字符串,相比于传统的 32 位或 64 位整数 ID,它占用了更多的存储空间。这可能会增加数据库和存储系统的负担
- 可读性差: UUID 的格式通常是一长串的字符,可读性差
- 无序: 基于时间的 UUID 虽然具有一定的有序性,但由于其包含了时间戳、机器 MAC 地址和随机数等多个部分,因此并不完全按照时间顺序排列。这可能对于某些需要按时间顺序处理数据的场景造成不便
2、Snowflake 雪花算法
Snowflake算法描述:指定机器 & 同一时刻 & 某一并发序列,是唯一的。据此可生成一个64 bits的唯一ID(long)。默认采用上图字节分配方式,除了sign:1bit序号位不可调整外,其他的标识都可以根据实际情况做些调整。
- sign(1bit):固定1bit符号标识,表示正式,业务中常用于标识生成的UID为正数
- timestamp(41bit):用于表示时间戳,相对于某个时间基点的增量值,单位是毫秒,可以支持 (1L<<41)/1000L360024*365 ≈ 69年
- datacenter id + worker id (10bit):在分布式IDC环境下,一般用前5位标识机房id/大区id,后五位表示机器id,用来区分不同的集群/机房的节点,这样可以表示32个IDC,每个IDC下面可以有32个机器
- sequence(12bit):用来表示每毫秒下的并发序列,序列号为自增值,12个bit代表单台机器每毫秒能够生成2^12=4096个ID,理论上snowflake方案的QPS约为409.6w/s
- 可支持单业务单大区重启 2^17-1=131071次
优点:生成速度比较快,毫秒数在高位,自增序列号在低位,整体ID是趋势递增的,业务需要时可以加上业务ID
缺点:强依赖机器时钟,如果机器上时钟回拨(即服务器上的时间突然倒退回之前的时间),进而导致发号重复或者服务会处于不可用状态
3、PearFlower 梨花算法
雪花算法的改进方法,目标:解决雪花算法的时间回拨问题,秒级时间戳回避毫秒级时间回拨问题;随机段号降低相同机器ID造成的影响
原理
- 符号位:1位
- 时间:31位,精确到秒级,可以用68年
- 段号(批次号):3位 每秒可分为8个段
- 机器号:10位 最多支持1024台机器
- sequence:19位 可表示:0–524287
优化点
- 经过调整,时间只对秒灵敏,成功回避了服务器间几百毫秒的时间误差引起的时间回拨问题;
- 若第59秒的8个段号没有用完,则当润秒来临时,还可继续使用
- 可设置一定的秒数(如3秒)内提前消费。比如第10秒的号码,在800毫秒用完了,可以继续使用第11秒的号码。这样,下1秒用的号码不是很多时,就可以借给上1秒使用。
参考:分布式全局唯一ID生成算法(改进的雪花算法——解决时钟回拨问题) https://blog.csdn.net/abckingaa/article/details/106460583
4、Mist 薄雾算法
原理
- 第一段为最高位,占 1 位,保持为 0,使得值永远为正数;
- 第二段放置自增数,占 47 位,自增数在高位能保证结果值呈递增态势,遂低位可以为所欲为;
- 第三段放置随机因子一,占 8 位,上限数值 255,使结果值不可预测;
- 第四段放置随机因子二,占 8 位,上限数值 255,使结果值不可预测;
优点:递增态势、不依赖数据库、高性能;不受时间回拨的影响
缺点:程序重启会造成按序递增数值回到初始值,要借助存储在程序重启时恢复自增序列。
三、常见发号器服务
1、数据库
1)自增
原理:利用给字段设置auto_increment_increment和auto_increment_offset来保证ID自增
- 建表:
CREATE TABLE `sequence_id` (
`id` bigint(20) unsigned NOT NULL AUTO_INCREMENT,
`stub` char(10) NOT NULL DEFAULT '',
PRIMARY KEY (`id`),
UNIQUE KEY `stub` (`stub`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
创建一张序号表,stub字段无实际意义,只是为了占位,便于我们插入或者修改数据。并且,给 stub 字段创建了唯一索引,保证其唯一性。
- SQL
BEGIN;
REPLACE INTO sequence_id (stub) VALUES ('stub');
SELECT LAST_INSERT_ID();
COMMIT;
DELETE FROM sequence_id WHERE id = LAST_INSERT_ID();
第一步:尝试把数据插入表中
第二步:如果主键或唯一索引的字段因出现重复数据而插入失败时,先从表中删除含有重复关键字的冲突行,然后再次尝试把数据插入表中。
优点
- 非常简单,利用现有数据库系统的功能实现,成本小,存储空间消耗小,有DBA专业维护
- ID号单调自增,可以实现一些对ID有特殊要求的业务
缺点
- 支持的并发量不大, 强依赖DB,当DB异常时整个系统不可用,属于致命问题,配置主从复制可以尽可能的增加可用性,但是数据一致性在特殊情况下难以保证,主从切换时的不一致可能会导致重复发号。
- 存在数据库单点问题,ID发号性能瓶颈限制在单台MySQL的读写性能 ,可以用数据库集群解决,不过增加了系统复杂度。如在分布式系统中每台机器设置不同的id初始值,且步长和机器数相等,不过系统维护困难,系统水平扩展比较困难,比如定义好了步长和机器台数之后,不容易扩展
- 数据库压力大,每次获取ID都得读写一次数据库,只能靠堆机器来提高性能
- ID没有具体的业务含义,而且存在安全问题,自增的订单ID存在规律,可以推测出业务数据,比如每天订单量等
2)号段模式
原理:
从数据库批量的获取自增ID,每次从数据库取出一个号段范围,例如 (1,1000] 代表1000个ID,具体的业务服务将使用本号段,生成1~1000的自增ID并加载到内存,从内存获取速度很快
- 建表
CREATE TABLE `sequence_id_generator` (
`id` int(10) NOT NULL,
`current_max_id` bigint(20) NOT NULL COMMENT '当前最大id',
`step` int(10) NOT NULL COMMENT '号段的长度',
`version` int(20) NOT NULL COMMENT '版本号',
`biz_type` int(20) NOT NULL COMMENT '业务类型',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
biz_type :代表不同业务类型
current_max_id:当前最大的可用id
step :代表号段的长度
version :是一个乐观锁,每次都更新version,保证并发时数据的正确性
current_max_id 字段和step字段主要用于获取批量 ID,获取的批量 id 为:current_max_id ~ current_max_id+step。
- 先新增一行数据
INSERT INTO `sequence_id_generator` (`id`, `current_max_id`, `step`, `version`, `biz_type`)
VALUES
(1, 0, 100, 0, 101);
- 通过 SELECT 获取指定业务下的批量唯一 ID
UPDATE sequence_id_generator SET current_max_id = current_max_id +step, version=version+1 WHERE version = 0 AND `biz_type` = 101
SELECT `current_max_id`, `step`,`version` FROM `sequence_id_generator` where `biz_type` = 101
结果:id current_max_id step version biz_type
1 100 100 1 101
- update current_max_id = current_max_id + step,update成功则说明新号段获取成功,新的号段范围是(current_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生成方式不强依赖于数据库,不会频繁的访问数据库,对数据库的压力小很多
- 不够用的话,更新之后重新 SELECT 即可。
等这批号段ID用完,再次向数据库申请新号段,对current_max_id 字段做一次update操作
优点:比于数据库主键自增的方式,数据库的号段模式对于数据库的访问次数更少,数据库压力更小,存储消耗空间更小
缺点:跟自增模式类型,同样存在数据库单点问题、ID 没有具体业务含义、安全问题、
2、NoSQL
以使用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持久化