跳至主要內容

MySQL-高可用篇-③分布式ID(发号器)

holic-x2024年7月10日...大约 24 分钟JAVAMySQL

MySQL-高可用篇-③分布式ID(发号器)

学习核心

学习资料

分布式ID核心概念

什么是分布式ID?(前面分库分表中提到的全局唯一ID概念)

​ 在分布式系统中,需要对大量的数据和消息进行唯一标识,例如订单ID、分布式IM系统中的消息ID等。随着产品的使用,数据日渐增长,在一定规模场景需求下需要对数据进行拆分(即分库分表),并且需要通过一个唯一ID来标识一条数据或者消息。在这个场景下,因为不同数据库的自增ID会发生冲突,所以单纯使用数据库的自增ID显然无法满足需求,此时拥有个生成全局唯一ID的方案是非常必要的。

分布式ID的特性

分布式ID生成方案

​ 传统的单表自增主键ID方案已经无法满足分布式的需求场景,此处介绍几种常见的分布式ID主流实现方案

1.UUID

生成策略

​ UUID(Universally Unique Identifier)通用唯一标识符的缩写。其标准形式包括32个16进制数字,形式为8-4-4-4-12,业界目前共有5种方式生成UUID,可参考IETF发布的UUID规范open in new window

import java.util.UUID;
public class UuidDemo {
    public static void main(String[] args) {
        System.out.println(UUID.randomUUID());
    }
}

// output
0c620ace-ec44-4c08-bb57-84b4d85acf21

优缺点

不同版本的UUID生成策略

2.雪花算法(Snowflake)

生成策略

Snowflakeopen in new window 是 Twitter 开源的分布式 ID 生成算法,由 64 位的二进制数字组成,一共分为 4 部分。受雪花算法开源之后的影响,各大公司也相继开发出各具特色的分布式生成器(例如美团开发的Leaf)

​ Snowflake和UUID一样,都是属于本地生成ID。Snowflake生成的是Long类型的ID(64位),按照一定的规则进行分段填充:

image-20240710082652949

优缺点

什么是时钟回拨问题

​ 时钟回拨:时间回拨到过去的某一个时间点,时钟回拨出现的两种场景:使用NTP机制进行校准时、闰秒

​ 因为服务器的本地时钟并不是绝对准确的,每一台计算机都有自己的本地时钟,这个时钟是根据 CPU 的晶振脉冲计算得来的,然而随着运行时间的推移,这个时间和世界时间的偏差会越来越大,就需要利用到NTP服务来进行时钟校准NTP(Network Time Protocol)服务自动校准就可能导致时钟回拨。

​ 在一些业务场景中,比如在电商的整点抢购中,为了防止不同用户访问的服务器时间不同,则需要保持服务器时间的同步。为了确保时间准确,会通过 NTP 的机制来进行校对,NTP(Network Time Protocol)指的是网络时间协议,用来同步网络中各个计算机的时间。如果服务器在同步 NTP 时出现不一致,出现时钟回拨,那么 SnowFlake 在计算中可能出现重复 ID。

​ 除了 NTP 同步,闰秒也会导致服务器出现时钟回拨。

解决方案:不过时钟回拨是小概率事件,在并发比较低的情况下一般可以忽略。关于如何解决时钟回拨问题,可以进行延迟等待,直到服务器时间追上来为止

3.单表生成自增主键(MySQL)

生成策略

​ 思考一个问题:既然多张单表生成的自增主键会冲突,如果所有表中的主键都从一张单表中生成是不是就可行了?以订单表为例,生成一个订单ID表

CREATE TABLE IF NOT EXISTS `order_sequence`(
   `order_id` INT UNSIGNED AUTO_INCREMENT,
   PRIMARY KEY ( `order_id` )
)ENGINE=InnoDB DEFAULT CHARSET=utf8;

​ 理论可行:可实现唯一、自增,但是这个用于生成主键的单表会存在两个问题:

​ 上述只是一个实现思路,如果要采用这种策略的话,其完善的设计和使用参考如下:

# 1.创建1个用于生成自增主键的单表,id为要使用的主键ID,stub可以理解为一个占位符概念(也可以赋予它一个业务概念,例如说明主键ID用途)
CREATE TABLE IF NOT EXISTS `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;

# 2.通过replace into来插入数据
BEGIN;
# 插入数据(以用于生成新的主键)
REPLACE INTO sequence_id(stub) values('分布式ID');
# 查看上一个插入的ID
SELECT LAST_INSERT_ID();
COMMIT;

​ 可以查看一下效果,例如初始化该表是没有任何数据的,执行一下步骤【2】

​ 看到此处会有两个问题产生:a.为什么要用replace而不是用insert? b.stub在这个表中的意义是什么?

优缺点

性能优化

​ 对于MySQL-单表生成自增ID主键的方案,可以在分布式系统中多部署几台机器,每台机器设置不同的初始值、设置步长(步长和机器数相同),进而将单台实例访问压力分散到各个节点。例如有4台实例,则其ID生成策略设置参考如下:

​ 以此类推,要部署N台机器,则其发号规则为:

​ 通过分布式部署可以减轻单个数据库访问压力,但其生成的ID整体呈现是趋势递增,且该优化方向是通过堆机器来提高性能,数据库压力还是很大(每次读写都要访问数据库)。如果遇到扩容场景,维护工作压力也会很大

4.数据库维护自增ID区间(占据ID区间)

生成策略

​ 设置每个实例的起始值和步长,只要确保每个实例生成的主键范围不重合即可,例如此处拆分为4个实例:

​ 如果说实例1的ID已经用到了1999怎么办?类似的,重新生成一个起始值,然后按照一定步长来约定每个实例的主键ID生成范围。以此类推

​ 其实现思路是通过维护一个sequence表实现: sequence 表中的每一行,用于记录某个业务主键当前已经被占用的 ID 区间的最大值。sequence 表的主要字段是 name 和 value,其中 name 是当前业务序列的名称,value 存储已经分配出去的 ID 最大值

CREATE TABLE `sequence` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Id',
  `name` varchar(64) NOT NULL COMMENT 'sequence name',
  `value` bigint(32) NOT NULL COMMENT 'sequence current value',
   PRIMARY KEY (`id`),
  UNIQUE KEY `unique_name` (`name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8; 

​ 当服务器(例如实例1)获取主键增长区段时,首先访问对应数据库的 sequence 表,更新对应的记录,占用一个对应的区间。比如设置步长为 200,原先的 value 值为 1000,更新后的 value 就变为了 1200。当实例获取到对应的 ID 区间后,在服务器内部可以使用AtomicInteger等方式进行ID分配,涉及的并发问题可以依赖乐观锁等机制解决。

​ 不同的服务器在相同的时间内分配出去的 ID 可能不同,这种方式生成的唯一 ID,不保证严格的时间序递增,但是可以保证整体的趋势递增,在实际生产中有比较多的应用。为了防止单点故障,sequence 表所在的数据库,通常会配置多个从库,实现高可用

优缺点

5.Redis(多实例自增+步长)

生成策略

​ 通过Redis的incr命令实现ID的顺序递增,用于生成分布式ID。该策略和MySQL策略类似,只不过此处将MySQL数据库替换为Redis数据库,概念是类似的

set ID 1
get ID
incr ID
incr ID

​ 当商品服务有多个的时候,其实也可以利用多台Redis机器来生成分布式ID。那么此处需要考虑不同的Redis生成的ID如何保证不重复?

image-20240710101536008

解决方案:针对不同的Redis实例设置不一样的起始值,并设置"大小等于机器数量"的"步长"

​ 例如假设现在有4台Redis机器,则设定如下:

set ID 1
get ID
incrby ID 4
incrby ID 4 

​ 基于这种策略实现还需要考虑两个问题:一个是扩容问题、一个是Redis的持久化问题

优缺点

6.Leaf

​ Leaf是美团技术团队开源的一个分布式ID生成服务,名字取自德国哲学家、数学家菜布尼茨的一句话:"There areno two identical leaves in the world."("世界上没有两片相同的叶子"),也代表着Leaf不会生成重复的ID。对于目前常应用在生产中的的分布式ID生成方式,都是基于数据库号段模式和雪花算法(snowflake),而leaf刚好同时兼具了这两种方式,可以根据不同业务场景灵活切换,把这两种模式分别称为Leaf-segment和Leaf-snowflake

Leaf-segment(分段获取ID号段)

​ Leaf-segment 实际上可以看做是MySQL模式的一种改进方案。

CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128) NOT NULL DEFAULT '', -- your biz unique name`max_id` bigint(20) NOT NULL DEFAULT '1',
  `step` int(11) NOT NULL,
  `max_id` bigint(20) DEFAULT 1,
  `description` varchar(256) DEFAULT NULL,
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;

- biz_tag:用于区分业务
- max_id:表示该biz_tag目前所被分配的ID号段的最大值
- step:step表示每次分配的号段长度

insert into leaf_alloc(biz_tag,step,max_id) values('pay_ordertag',1000,3000);
insert into leaf_alloc(biz_tag,step,max_id) values('waimai_ordertag',2000,10000);
insert into leaf_alloc(biz_tag,step,max_id) values('banma_ordertag',20000,20000);
insert into leaf_alloc(biz_tag,step,max_id) values('test_tag',1000,3000);
select * from leaf_alloc;

image-20240710110532943

​ 基于上述图示分析,有3台Leaf机器,从左到右:Leaf01、Leaf02、Leaf03

​ 其中test_tag在Leaf01机器上的1-1000的号段,当这个号段用完之后就会向数据库申请一个长度为1000的号段,申请成功后Leaf01机器新加载的号段为3001~4000,相应更新biz_tag为test_tag的数据(其max_id会从3000更新为4000)

BEGIN ;
UPDATE leaf_alloc SET max_id = max_id + step where biz_tag = 'test_tag';
SELECT biz_tag,max_id,step from leaf_alloc where biz_tag = 'test_tag';
COMMIT;

​ 如果单纯使用上述方案,ID生成的性能容易产生波动。因为当某个Leaf机器号段使用完之后,会等待在更新数据库的I/O上,所以 Leaf-segment 还做了进一步的优化-双buffer优化(用于优化号段用尽时更新号段所造成的性能阻塞)

双buffer优化的思路:

image-20240710121441076

​ 可以从上图看出,对于某一个biz tag,Leaf服务内部可以缓存两个号段(segment),当号段已下发10%时,如果下一个号段未更新,则另启一个线程去获取下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发ID,循环往复。

优缺点

⚽️滴滴TinyID方案(基于 Leaf-segment 升级版本)

​ Tinyid 方案是在 Leaf-segment 的算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了 tinyid-client 客户端的接入方式,使用起来更加方便。

​ Tinyid 会将可用号段加载到内存中,并在内存中生成 ID,可用号段在首次获取 ID 时加载,如当前号段使用达到一定比例时,系统会异步的去加载下一个可用号段,以此保证内存中始终有可用号段,以便在发号服务宕机后一段时间内还有可用 ID。

image-20240710122040527

Leaf-snowflake

​ 针对上述 Leaf-segment 方案的缺点问题(ID生成不够随机),可能会存在一种泄露信息的危险。例如针对一些订单ID或者其他相关的ID生成场景,可通过订单号ID相减来大致估算公司一天的订单量,容易泄露秘密。因此通过引入Leaf-snowflake来应用上述问题。

​ Leaf-snowflake是完全沿用snowflake方案的bit位设计,主要进行了两点优化:

Leaf-snowflake方案启动过程:

image-20240710113759590

优缺点

分布式ID生成方案总结

分布式ID生成方案生成方式优点缺点
UUID本地生成(String)生成简单、性能不错
可本地生成,没有网络消耗
可读性差:缺乏业务含义
存储成本高:UUID太长(16字节128位),通常以36长度的字符串来表示,很多场景不适用
存在性能问题:UUID是随机的,将记录随机插入到数据库中容易产生页分裂问题
信息安全问题:基于MAC地址生成UUID的算法可能会造成MAC地址泄露
雪花算法(Snowflake)本地生成(Long)或集群部署生成ID的性能很高

按照时间整体有序(趋势递增)
可以根据自身业务特性分配bit位(workId部分),非常灵活
强依赖于机器时钟,存在时钟回拨问题
单表生成自增主键(MySQL)MySQL实现简单,通过单表确保ID有序递增
存储消耗空间小
强依赖于数据库,存在单点故障问题
ID发号的性能瓶颈被限制在单台MySQL的读写性能
数据库维护自增ID区间MySQL可实现主键的唯一性,但不保证严格的连续递增(某段时间内递增)强依赖于数据库,可搭配多个从库使用以支持高可用
RedisRedis实现简单
整体吞吐量比数据库要高
Redis实例或集群宕机后,找到最新的ID值会有点困难
Leaf-segment服务部署Leaf-segment服务可以很方便扩展机器,性能完全能够支撑大多数业务场景
容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服
ID号码不够随机,容易泄露发号数量的信息,不太安全
强依赖DB,如果DB宕机会造成整个系统不可用(实际上用到数据库的方案都有可能)
Leaf-snowflake服务部署+集群(Zookeeper)ID生成按照时间整体有序,且每一个ms会随机起始序列号,ID安全性较好
不会出现因时钟回拨而导致ID生成异常问题(例如ID冲突、ID乱序)
依赖于Zookeeper,存在服务不可用风险
发生时钟回拨时,服务会短暂不可用
评论
  • 按正序
  • 按倒序
  • 按热度
Powered by Waline v3.1.3