MySQL-高可用篇-③分布式ID(发号器)
MySQL-高可用篇-③分布式ID(发号器)
学习核心
分布式ID概念核心
分布式ID生成方案
分布式ID生成系统
学习资料
分布式ID核心概念
什么是分布式ID?(前面分库分表中提到的全局唯一ID概念)
在分布式系统中,需要对大量的数据和消息进行唯一标识,例如订单ID、分布式IM系统中的消息ID等。随着产品的使用,数据日渐增长,在一定规模场景需求下需要对数据进行拆分(即分库分表),并且需要通过一个唯一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规范
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太长(16字节128位),通常以36长度的字符串来表示,很多场景不适用
- 存在性能问题:UUID是随机的,将记录随机插入到数据库中容易产生页分裂问题
- 信息安全问题:基于MAC地址生成UUID的算法可能会造成MAC地址泄露
不同版本的UUID生成策略
- 基于时间的 UUID:主要依赖当前的时间戳和机器 mac 地址。优势是能基本保证全球唯一性,缺点是由于使用了 mac 地址,会暴露 mac 地址和生成时间;
- 分布式安全的 UUID:将基于时间的 UUID 算法中的时间戳前四位替换为 POSIX 的 UID 或 GID。优势是能保证全球唯一性,缺点是很少使用,常用库基本没有实现;
- 基于随机数的 UUID:基于随机数或伪随机数生成。优势是实现简单,缺点是重复几率可计算;
- 基于名字空间的 UUID(MD5 版):基于指定的名字空间/名字生成 MD5 散列值得到。优势是不同名字空间/名字下的 UUID 是唯一的,缺点是 MD5 碰撞问题,只用于向后兼容;
- 基于名字空间的 UUID(SHA1 版):将基于名字空间的 UUID(MD5 版)中国的散列算法修改为 SHA1。优势是不同名字空间/名字下的 UUID 是唯一的,缺点是 SHA1 计算相对耗时。
2.雪花算法(Snowflake)
生成策略
Snowflake 是 Twitter 开源的分布式 ID 生成算法,由 64 位的二进制数字组成,一共分为 4 部分。受雪花算法开源之后的影响,各大公司也相继开发出各具特色的分布式生成器(例如美团开发的Leaf)
Snowflake和UUID一样,都是属于本地生成ID。Snowflake生成的是Long类型的ID(64位),按照一定的规则进行分段填充:
- 正数位(1 bit)+ 时间戳(41 bit)+ 工作机器ID(10 bit)+ 序列号(12 bit)
1 位 正数位:第 1 位默认不使用,作为符号位(0-正数;1-负数),一般生成ID都是正数,此处设置为0 以保证数值是正数;
41 位时间戳:表示毫秒数,41 位数字可以表示 241 毫秒(换算:(1L << 41 )/(1000L × 60 × 60 × 24 × 365)= 69年)
- 一般不建议存储当前时间戳(会损耗一些ID),而是用(当前时间戳-固定开始时间)的差值,可以使产生的ID从更小的值开始
10 位工作机器 ID:workId,支持 210 也就是 1024 个节点(最大可配置1024台机器);
- workId可以灵活配置,例如可以是机房ID+机器ID的组合
12 位序列号:作为当前时间戳和机器下的流水号,每个节点每毫秒内支持 212 的区间,也就是 4096 个 ID,换算成秒,相当于可以允许 409 万的 QPS,如果在这个区间内超出了 4096,则等待至下一毫秒计算。
- 自增值支持同一毫秒内同一节点可以生成4096个ID
优缺点
优点
- 本地生成(可通过类似UUID那样调用方法生成,也可作为一个单独的服务进行集群部署),不依赖于第三方系统,生成ID的性能很高
- 按照时间整体有序(趋势递增)
- 可以根据自身业务特性分配bit位(workId部分),非常灵活
缺点
- 强依赖于机器时钟,存在时钟回拨问题(如果机器上出现时钟回拨,会导致发号重复或者服务处于不可用状态)
- 当出现时钟回拨问题时,可以进行延迟等待,知道服务器时间追上来为止
- 强依赖于机器时钟,存在时钟回拨问题(如果机器上出现时钟回拨,会导致发号重复或者服务处于不可用状态)
什么是时钟回拨问题?
时钟回拨:时间回拨到过去的某一个时间点,时钟回拨出现的两种场景:使用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】
- 第1次执行:执行前表内没有任何数据,执行后插入一条数据,检索结果为1 (可以理解为某个业务占用了这个ID值,要使用这个ID值)
- 第2次执行:执行前表内已有数据,执行后通过replace指令替换记录,检索结果为2(类似地,某个业务占用了这个ID值为2的主键)
- 以此类推,业务通过调用步骤【2】的语句即可获取到相应的自增主键ID,且可确保全局唯一。这种策略的做法是每次访问都先生成/占领一个新的主键,不care上一个主键是不是真的被用到,只关注当前生成的新主键(replace语句的作用用于生成新主键)
看到此处会有两个问题产生:a.为什么要用replace而不是用insert? b.stub在这个表中的意义是什么?
a.为什么要用replace而不是用insert? =》节省存储空间
- 从上述测试结果可以看到,使用replace 语句每次更新的都是同一条记录,它的执行过程是会尝试将数据先插入到表中,如果因为插入冲突导致插入失败的话,则会删除掉冲突行并再次尝试插入。这样就可以确保这个”插入“操作可以”占据“目前最新的主键ID 以供业务使用,并确保表中永远只有一条记录,以节省内存空间
- 实际上insert也可以达到同样的效果,但不同点在于insert每次都会直接插入,就会导致表中产生很多记录,而这些记录都不是关心的重点,重点在于要拿到最新的ID值
b.stub在这个表中的意义是什么?=》无意义,纯粹占位符
- 一开始可能会将其理解为用于区分不同机器、业务场景的字段,赋予它一些业务含义,但是实际上要理解这个单表ID生成策略面向的对象是谁
- 如果说赋予了stub一些业务含义用于区分不同的业务,则演变成这张表生成的主键ID是提供给多个不同的业务使用(例如订单、库存等等),但实际上这并不是预期中的设定。例如在对订单表进行分表,则预期中每个用作生成全局唯一ID的单表它都是面向订单号这个ID的(而不是将库存ID、流水号ID这类也混在一起)。如果说多个不同业务都采用同一个表生成的ID,管理起来反而混乱,可以参考下述示例
- 如果说赋予了stub一些业务含义用于区分不同的机器,则看其实现的必要性
- 实际上,stub可以很纯粹地理解为一个占位符概念,如果说非要赋予它一些含义,可以把它当做一个说明/描述字段,用于描述这个ID的作用(必须保证唯一)
优缺点
- 优点
- 实现简单,通过单表确保ID有序递增
- 存储消耗空间小(如果每次使用replace into插入数据都是一致的,表中只会存在一条记录,占用空间很小)
- 缺点
- 强依赖于数据库,存在单点故障问题,如果数据库系统异常,就会导致系统业务的创建功能不可用
- ID发号的性能瓶颈被限制在单台MySQL的读写性能
性能优化
对于MySQL-单表生成自增ID主键的方案,可以在分布式系统中多部署几台机器,每台机器设置不同的初始值、设置步长(步长和机器数相同),进而将单台实例访问压力分散到各个节点。例如有4台实例,则其ID生成策略设置参考如下:
- 实例1:起始值1,步长4 =》生成ID:1、5、9、13、17、21、........
- 实例2:起始值2,步长4 =》生成ID:2、6、10、14、18、22、......
- 实例3:起始值3,步长4 =》生成ID:3、7、11、15、19、23、......
- 实例4:起始值4,步长4 =》生成ID:4、8、12、16、20、24、......
以此类推,要部署N台机器,则其发号规则为:
- 实例N:起始值N-1,步长N
通过分布式部署可以减轻单个数据库访问压力,但其生成的ID整体呈现是趋势递增,且该优化方向是通过堆机器来提高性能,数据库压力还是很大(每次读写都要访问数据库)。如果遇到扩容场景,维护工作压力也会很大
4.数据库维护自增ID区间(占据ID区间)
生成策略
设置每个实例的起始值和步长,只要确保每个实例生成的主键范围不重合即可,例如此处拆分为4个实例:
- 实例1:起始1000,步长1000 =》 1000-1999
- 实例2:起始2000,步长1000 =》 2000-2999
- 实例3:起始3000,步长1000 =》 3000-3999
- 实例4:起始4000,步长1000 =》 4000-4999
如果说实例1的ID已经用到了1999怎么办?类似的,重新生成一个起始值,然后按照一定步长来约定每个实例的主键ID生成范围。以此类推
- 实例1:起始5000,步长1000 =》 5000-5999
- 实例2:起始6000,步长1000 =》 6000-6999
- 实例3:起始7000,步长1000 =》 7000-7999
- 实例4:起始8000,步长1000 =》 8000-8999
其实现思路是通过维护一个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如何保证不重复?
解决方案:针对不同的Redis实例设置不一样的起始值,并设置"大小等于机器数量"的"步长"
例如假设现在有4台Redis机器,则设定如下:
- 实例1:起始值1,步长4 =》生成ID:1、5、9、13、17、21、........
- 实例2:起始值2,步长4 =》生成ID:2、6、10、14、18、22、......
- 实例3:起始值3,步长4 =》生成ID:3、7、11、15、19、23、......
- 实例4:起始值4,步长4 =》生成ID:4、8、12、16、20、24、......
set ID 1
get ID
incrby ID 4
incrby ID 4
基于这种策略实现还需要考虑两个问题:一个是扩容问题、一个是Redis的持久化问题
- 当访问量增大,可能需要更多的Redis机器来生成分布式ID,也需要重新改变Redis机器ID起始值和步长
- Redis的持久化有两种方式:
- RDB:快照持久化,每隔一段时间保存数据库快照,从而进行持久化,假如Redis没有及时持久化一段时间内的自增数据,而这时Redis挂掉了,重启Redis后可能会出现ID重复的情况
- AOF:增量持久化,执行命令后,会将命令也写入到AOF文件中,可以弥补RDB持久化实时性的不足,但仍然存在丢失数据的可能性
优缺点
- 优点
- 实现简单
- 整体吞吐量比数据库要高
- 缺点
- Redis实例或集群宕机后,找到最新的ID值会有点困难
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模式的一种改进方案。
- 原方案:每次获取ID都要读取一次数据库,造成数据库压力巨大
- 优化方案:改为利用proxy server批量获取,每次获取一个segment(step决定大小)号段的值,用完之后再去数据库获取新的号段,以减轻数据库的压力
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;
基于上述图示分析,有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优化的思路:
- 原始方案:Leaf 取号段是需要等待一个号段消耗完的时候才会进行,那么从DB取回新号段的时间就决定了号段交替时ID下发所花费时间,如果号段交替期间有新的请求进来,那么也会因为号段没有交替完成而让线程阻塞等待,包括DB的网络情况、DB的性能都是会造成一些影响
- 优化思路:提前加载号段=>为了让DB交替号段的过程做到无阻塞,可以当号段消费到某个点时就异步的把下一个号段加载到内存中,而不用等到号段用尽的时候才去更新号段
可以从上图看出,对于某一个biz tag,Leaf服务内部可以缓存两个号段(segment),当号段已下发10%时,如果下一个号段未更新,则另启一个线程去获取下一个号段。当前号段全部下发完后,如果下个号段准备好了则切换到下个号段为当前segment接着下发ID,循环往复。
- 每个biz-tag都有消费速度监控,通常推荐segment长度设置为服务高峰期发号QPS的600倍(10分钟),这样即使DB宕机,Leaf仍能持续发号10-20分钟不受影响
- 每次请求来临时都会判断下个号段的状态,从而更新此号段,所以偶尔的网络抖动不会影响下个号段的更新
优缺点
- 优点
- Leaf-segment服务可以很方便扩展机器,性能完全能够支撑大多数业务场景
- 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服
- 缺点
- ID号码不够随机,容易泄露发号数量的信息,不太安全
- 强依赖DB,如果DB宕机会造成整个系统不可用(实际上用到数据库的方案都有可能)
⚽️滴滴TinyID方案(基于 Leaf-segment 升级版本)
Tinyid 方案是在 Leaf-segment 的算法基础上升级而来,不仅支持了数据库多主节点模式,还提供了 tinyid-client 客户端的接入方式,使用起来更加方便。
Tinyid 会将可用号段加载到内存中,并在内存中生成 ID,可用号段在首次获取 ID 时加载,如当前号段使用达到一定比例时,系统会异步的去加载下一个可用号段,以此保证内存中始终有可用号段,以便在发号服务宕机后一段时间内还有可用 ID。
Leaf-snowflake
针对上述 Leaf-segment 方案的缺点问题(ID生成不够随机),可能会存在一种泄露信息的危险。例如针对一些订单ID或者其他相关的ID生成场景,可通过订单号ID相减来大致估算公司一天的订单量,容易泄露秘密。因此通过引入Leaf-snowflake来应用上述问题。
Leaf-snowflake是完全沿用snowflake方案的bit位设计,主要进行了两点优化:
- 对于其中workerlD的分配,Leaf-snowflake使用Zookeeper持久顺序节点的特性自动对snowflake节点配置wokerlD
- 对于原生snowflake可能存在的时钟回拨问题进行优化(强制等待)
Leaf-snowflake方案启动过程:
启动Leaf-snowflake服务,连接Zookeeper,在leaf_forever父节点下检查自己是否已经注册过(是否有该顺序子节点)
如果有注册过,就直接取回自己的workerlD(Zookeeper顺序节点生成的int类型ID号),启动服务
如果没有注册过,就在该父节点下面创建一个持久顺序节点,创建成功后取回顺序号当做自己的workerlD号,启动服务
优缺点
- 优点
- ID生成按照时间整体有序,且每一个ms会随机起始序列号,ID安全性较好
- 不会出现因时钟回拨而导致ID生成异常问题(例如ID冲突、ID乱序)
- 缺点
- 依赖于Zookeeper,存在服务不可用风险
- 发生时钟回拨时,服务会短暂不可用
分布式ID生成方案总结
分布式ID生成方案 | 生成方式 | 优点 | 缺点 |
---|---|---|---|
UUID | 本地生成(String) | 生成简单、性能不错 可本地生成,没有网络消耗 | 可读性差:缺乏业务含义 存储成本高:UUID太长(16字节128位),通常以36长度的字符串来表示,很多场景不适用 存在性能问题:UUID是随机的,将记录随机插入到数据库中容易产生页分裂问题 信息安全问题:基于MAC地址生成UUID的算法可能会造成MAC地址泄露 |
雪花算法(Snowflake) | 本地生成(Long)或集群部署 | 生成ID的性能很高 按照时间整体有序(趋势递增) 可以根据自身业务特性分配bit位(workId部分),非常灵活 | 强依赖于机器时钟,存在时钟回拨问题 |
单表生成自增主键(MySQL) | MySQL | 实现简单,通过单表确保ID有序递增 存储消耗空间小 | 强依赖于数据库,存在单点故障问题 ID发号的性能瓶颈被限制在单台MySQL的读写性能 |
数据库维护自增ID区间 | MySQL | 可实现主键的唯一性,但不保证严格的连续递增(某段时间内递增) | 强依赖于数据库,可搭配多个从库使用以支持高可用 |
Redis | Redis | 实现简单 整体吞吐量比数据库要高 | Redis实例或集群宕机后,找到最新的ID值会有点困难 |
Leaf-segment | 服务部署 | Leaf-segment服务可以很方便扩展机器,性能完全能够支撑大多数业务场景 容灾性高:Leaf服务内部有号段缓存,即使DB宕机,短时间内Leaf仍能正常对外提供服 | ID号码不够随机,容易泄露发号数量的信息,不太安全 强依赖DB,如果DB宕机会造成整个系统不可用(实际上用到数据库的方案都有可能) |
Leaf-snowflake | 服务部署+集群(Zookeeper) | ID生成按照时间整体有序,且每一个ms会随机起始序列号,ID安全性较好 不会出现因时钟回拨而导致ID生成异常问题(例如ID冲突、ID乱序) | 依赖于Zookeeper,存在服务不可用风险 发生时钟回拨时,服务会短暂不可用 |