发号器技术选型指南:UUID与数据库自增字段的优缺点分析
零、基本术语

用于生成唯一流水号(即俗称的唯一 ID)的服务系统被称作发号器。
[id_1756763923]
UUID
优点:能够保证唯一性
2. 数据无序会导致 B+树索引在写的时候有过多的随机写操作,不会产生有顺序的 append 操作,而是需要进行 insert 操作,这会读取整个 B+树节点到内存,在插入该条记录后会将整个节点写回磁盘,在记录占用空间比较大的情况下,写的性能会明显下降。
数据库
单库时:自增字段完全依赖数据库,这会带来一些局限性,比如在进行数据库移植、扩容、洗数据以及分库分表等操作时会出现问题。
分库分表时,采用水平伸缩的方式,即通过自增字段、数据库 seq 和步长 step 来实现。这种方式的缺陷在于服务节点是固定的,也就是说步长 step 是固定的,当继续增加服务节点时,难以进行扩展。同时,这种方式仍然强依赖于数据库,会对数据库造成压力。
开源项目-Snowflake
Twitter 开源的发号器存在一些缺点,其一在于其文档较为简单;其二在于其发布模式较为单一;其三在于它缺少相应的支持和维护。
自研的是一款通用且高性能的发号器产品,它具备“全局唯一、粗略有序、可反解、可制造”这些特性。同时,它有三种发布模式,分别是嵌入发布模式(以 jar 包的形式)、中心服务器发布模式(适用于服务化场景)、REST 发布模式(通过 rest api 接口)。
二、基本需求
全局唯一的情况是这样的:通常采用的悲观策略是使用锁或者分布式锁,不过这样会使性能大大降低。而在这里,会利用时间的有序性,在时间的某个单元之下,通过采用自增序列来实现全局唯一。
完全有序会涉及到数据的汇聚,并且需要用到锁或分布式锁。然而,考虑到效率问题,采用了折中方案,即粗略有序。这样就可以支持秒级有序和毫秒级有序两种方式。
可反解:ID 具有时间属性且可反解其他信息量,节省空间
可制造:即支持手工处理,可复制、可恢复、可制造
ID 生成与网络 I/O 和 CPU 的性能相关。网络 I/O 通常不会成为瓶颈。根据经验,单台机器的 TPS 能够达到 10000/s。
高可用:对等集群、重试机制、本地容错方案(即本地依赖)
可伸缩:水平伸缩,支持业务量增长
三、核心设计
发布模式
嵌入发布模式的情况是:仅适用于 java 客户端,通过嵌入 jar 包形式的原生服务,并且需要提前配置本地机器 ID。
中心服务器发布模式:仅适用于 java 客户端,会提供一个用于服务的客户端 jar 包,java 程序能够如同调用本地 api 那样进行调用,并且依赖于中心的 ID 来生成服务器。
中心服务器借助 Restful API 来导出服务,非 java 客户端能够对其进行使用,这就是 REST 发布模式。
ID 类型
最大峰值型的情况是:秒级是有序的,在秒级这个时间维度上占用 30 位,而序列号则占用 20 位。
字段版本类型生成方式秒级时间序列号机器 ID
位数
63
62
60-61
30-59
10-29
0-9
最小粒度型,其时间是以秒为单位进行有序排列的,秒级时间占用 40 位,序列号占用 10 位。
字段版本类型生成方式秒级时间序列号机器 ID
位数
63
62
60-61
20-59
10-19
0-9
数据结构
作为扩展位或扩容时的临时方案,有 1 位,其默认值为 0,当值为 1 时,表示正在进行扩展或扩容。
ID 类型:1 位,0-最大峰值型,1-最小粒度型
生成方式有 2 位,分别是 00 嵌入发布模式、01 中心发布模式、02(二进制 10)REST 发布模式、03(二进制 11)保留未用
时间方面,最大峰值型的秒级时间为 30 位,其时长为 2 的 30 次方除以 60 再除以 60 再除以 24 再除以 365,结果是 34 年;最小粒度型的毫秒级时间为 40 位,其时长为 2 的 40 次方除以 1000 再除以 60 再除以 60 再除以 24 再除以 365,同样是 34 年。
最大峰值型为 20 位,从理论上来说,每秒最多能够产生 2 的 20 次方,也就是 1048576 个 ID,这相当于百万级别;最小粒度型是 10 位,按照理论,每毫秒最多可以产生 2 的 10 次方,即 1024 个 ID,这样换算下来,每秒最多可达 1024000 个。
机器的 ID 是 10 位,这意味着最多能支持 1024 个服务器。中心发布模式和 REST 发布模式通常不会有太多数量的机器。根据设计,每台的 TPS 为 1 万/s,10 台服务器就能达到 10 万/s,这样已经可以满足绝大部分需求。而考虑到内嵌发布模式,对机器的需求量非常大,所以最多支持 1024 个服务器。
并发
中心服务器和 REST 发布模式,其开销主要包含网络 I/O 以及 CPU 操作。ID 生成主要是内存到高速缓存的操作,不存在磁盘 I/O 操作,而网络 I/O 成为系统的瓶颈。不过相较于网络 I/O 而言,CPU 计算速度更是瓶颈所在。所以,ID 产生的服务采用多线程的方式。
有多种实现方式,能够实现 ID 生成过程里的竞争点 time 和 sequence 。
使用 concurrent 包中的 ReentrantLock 来实现互斥,这是一种默认的实现方式,旨在追求性能与稳定之间的折中方案
使用传统的 synchronized 来实现互斥,其性能比(1)稍微差一些。只要传入 JVM 参数 -Dvesta.sync.lock.impl.key=true ,就可以开启这种方式。
使用 concurrent 包的原子变量来实现互斥,其性能是非常高的。然而,在高并发环境下,CPU 负载会变得很高。可以通过传入 JVM 参数 -Dvesta.atomic.lock.impl.key=true 来开启这种方式。
机器 ID 的分配
嵌入发布模式下,会预先配置机器 ID,这种模式最多能够支持 924 台内嵌服务器。
中心服务器发布模式以及 REST 发布模式,最多能够支持 100 台。并且最大能够支持 100 乘以 1 万每秒,也就是 100 万每秒的 TPS。
可以通过动态调整两个区间的值来进行适应,并且各个垂直业务本身具有天生的隔离性,每个业务都能够适应最多 1024 台服务器。
有 3 种机器 ID 的分配方式,分别是共享数据库方式、IP 方式以及 Spring 配置文件方式(仅在测试时使用)。
时间同步
crontab 能够周期性地通过时间服务器虚拟集群来核准服务器时间,即使用 ntpdate -u pool.ntp.orgpool.ntp.org 这个命令。
未重启机器且调慢时间时,Vesta 会抛出异常并拒绝产生 ID;重启机器且调快时间后,能正常产生 ID;重启机器且调慢时间,可能会产生重复的 ID,系统管理员需保证避免这种情况;每 4 年进行一次同步闰秒,因为是调快 1 秒,所以调整后不会影响 ID 的产生
设计验证
构建一个 ID 时,依据不同信息进行分段。这个 ID 具有全局唯一的特性,同时还可以进行反解,并且能够被制造等。
使用秒级时间或毫秒级时间,并且利用时间单元内序列递增的方式,以此来保证 ID 大致上是有序的。
多线程处理中心服务器发布模式以及 REST 发布模式,通过三种实现方式来解决竞争点的互斥问题。
保证发布模式能最大程度地可用,也就是说,当某种模式不可用时,能够回退到使用本地预配的机器 ID。
机器号分为两个区段,一个区段是从 0 开始向上递增的;另一个区段是从 1024 开始向下递减的;并且它还能够动态地调整分界线,以此来满足可伸缩性的要求。
四、代码实现
代理模式
【读书系列】
《可伸缩服务架构:框架与中间件》这本书是由李艳鹏、杨彪、李海亮、贾博岩、刘淏共同创作的,由电子工业出版社出版。