共计 7670 个字符,预计需要花费 20 分钟才能阅读完成。
写在后面
mykit-serial 框架的设计参考了李艳鹏大佬开源的 vesta 框架,并彻底重构了 vesta 框架,借鉴了雪花算法(SnowFlake)的思维,并在此基础上进行了全面降级和优化。反对嵌入式(Jar 包)、RPC(Dubbo,motan、sofa、SpringCloud、SpringCloud Alibaba 等支流的 RPC 框架)、Restful API(反对 SpringBoot 和 Netty),可反对最大峰值型和最小粒度型两种模式。
开源地址:
GitHub:https://github.com/sunshinelyz/mykit-serial
Gitee:https://gitee.com/binghe001/mykit-serial
为何不必数据库自增字段?
如果在业务零碎中应用数据库的自增字段,自增字段齐全依赖于数据库,这在数据库移植,扩容,荡涤数据,分库分表等操作时带来很多麻烦。
在数据库分库分表时,有一种方法是通过调整自增字段或者数据库 sequence 的步长来达到跨数据库的 ID 的唯一性,但依然是一种强依赖数据库的解决方案,有诸多的限度,并且强依赖数据库类型,如果咱们想减少一个数据库实例或者将业务迁徙到一种不同类型的数据库上,那是相当麻烦的。
为什么不必 UUID?
UUID 尽管可能保障 ID 的唯一性,然而,它无奈满足业务零碎须要的很多其余个性,例如:工夫粗略有序性,可反解和可制作型。另外,UUID 产生的时候应用齐全的工夫数据,性能比拟差,并且 UUID 比拟长,占用空间大,间接导致数据库性能降落,更重要的是,UUID 并不具备有序性,这就导致 B + 树索引在写的时候会有过多的随机写操作(间断的 ID 会产生局部程序写),另外写的时候因为不能产生程序的 append 操作,须要进行 insert 操作,这会读取整个 B + 树节点到内存,而后插入这条记录后再将整个节点写回磁盘,这种操作在记录占用空间比拟大的状况下,性能降落比拟大。所以,不倡议应用 UUID。
须要思考的问题
既然数据库自增 ID 和 UUID 有诸多的限度,咱们就须要思考如何设计一款分布式全局惟一的序列号(分布式 ID)服务。这里,咱们须要思考如下一些因素。
全局惟一
分布式系统保障全局惟一的一个乐观策略是应用锁或者分布式锁,然而,只有应用了锁,就会大大的升高性能。
因而,咱们能够借鉴 Twitter 的 SnowFlake 算法,利用工夫的有序性,并且在工夫的某个单元下采纳自增序列,达到全局的唯一性。
粗略有序
UUID 的最大问题就是无序的,任何业务都心愿生成的 ID 是有序的,然而,分布式系统中要做到齐全有序,就波及到数据的汇聚,须要用到锁或者分布式锁,思考到效率,须要采纳折中的计划,粗略有序。目前有两种支流的计划,一种是秒级有序,一种是毫秒级有序,这里又有一个衡量和取舍,咱们决定反对两种形式,通过配置来决定服务应用其中的一种形式。
可反解
一个 ID 生成之后,ID 自身带有很多信息量,线上排查的时候,咱们通常首先看到的是 ID,如果依据 ID 就能晓得什么时候产生的,从哪里来的,这样一个可反解的 ID 能够帮上很多忙。
如果 ID 里有了工夫而且能反解,在存储层面就会省下很多传统的 timestamp 一类的字段所占用的空间了,这也是两全其美的设计。
可制作
一个零碎即便再高可用也不会保障永远不出问题,出了问题怎么办,手工解决,数据被净化怎么办,洗数据,可是手工解决或者洗数据的时候,如果应用数据库自增字段,ID 曾经被起初的业务笼罩了,怎么复原到零碎出问题的工夫窗口呢?
所以,咱们应用的分布式全局序列号(分布式 ID)服务肯定要可复制,可复原,可制作。
高性能
不论哪个业务,订单也好,商品也好,如果有新记录插入,那肯定是业务的外围性能,对性能的要求十分高,ID 生成取决于网络 IO 和 CPU 的性能,CPU 个别不是瓶颈,依据教训,单台机器 TPS 应该达到 10000/s。
高可用
首先,分布式全局序列号(分布式 ID)服务必须是一个对等的集群,一台机器挂掉,申请必须可能转发到其余机器,另外,重试机制也是必不可少的。最初,如果近程服务宕机,咱们须要有本地的容错计划,本地库的依赖形式能够作为高可用的最初一道屏障。
也就是说,咱们反对 RPC 公布模式,嵌入式公布模式和 REST 公布模式,如果某种模式不可用,能够回退到其余公布模式,如果 Zookeeper 不可用,能够会退到应用本地预配的机器 ID。从而达到服务的最大可用。
可伸缩
作为一个分布式系统,永远都不能疏忽的就是业务在一直地增长,业务的相对容量不是掂量一个零碎的唯一标准,要晓得业务是永远增长的,所以,零碎设计岂但要思考能接受的相对容量,还必须思考业务增长的速度,零碎的程度伸缩是否能满足业务的增长速度是掂量一个零碎的另一个重要规范。
设计与实现
整体架构设计
mykit-serial 的整体架构图如下所示。
mykit-serial 框架各模块的含意如下:
- mykit-bean:提供对立的 bean 类封装和整个框架应用的常量等信息。
- mykit-common:封装整个框架通用的工具类。
- mykit-config:提供全局配置能力。
- mykit-core:整个框架的外围实现模块。
- mykit-db:寄存数据库脚本。
- mykit-interface:整个框架的外围形象接口。
- mykit-service:基于 Spring 实现的外围性能。
- mykit-rpc:以 RPC 形式对外提供服服务(后续反对 Dubbo,motan、sofa、SpringCloud、SpringCloud Alibaba 等支流的 RPC 框架)。
- mykit-server:目前实现了 Dubbo 形式,后续迁徙到 mykit-rpc 模块。
- mykit-rest:基于 SpringBoot 实现的 Rest 服务。
- mykit-rest_netty:基于 Netty 实现的 Rest 服务。
- mykit-test:整个框架的测试模块,通过此模块能够疾速把握 mykit-serial 的应用形式。
公布模式
依据最终的客户应用形式,可分为嵌入公布模式,RPC 公布模式和 Rest 公布模式。
- 嵌入公布模式 :只实用于 Java 客户端,提供一个本地的 Jar 包,Jar 包是嵌入式的原生服务,须要提前配置本地机器 ID(或者服务启动时,由 Zookeeper 动态分配惟一的分布式序列号),然而不依赖于核心服务器。
- RPC 公布模式 :只实用于 Java 客户端,提供一个服务的客户端 Jar 包,Java 程序像调用本地 API 一样来调用,然而依赖于核心的分布式序列号(分布式 ID)产生服务器。
- REST 公布模式 :核心服务器通过 Restful API 提供服务,供非 Java 语言客户端应用。
公布模式最初会记录在生成的全局序列号中。
序列号类型
依据工夫的位数和序列号的位数,可分为最大峰值型和最小粒度型。
1. 最大峰值型 :采纳秒级有序,秒级工夫占用 30 位,序列号占用 20 位
字段 | 版本 | 类型 | 生成形式 | 秒级工夫 | 序列号 | 机器 ID |
---|---|---|---|---|---|---|
位数 | 63 | 62 | 60-61 | 30-59 | 10-29 | 0-9 |
2. 最小粒度型 :采纳毫秒级有序,毫秒级工夫占用 40 位,序列号占用 10 位
字段 | 版本 | 类型 | 生成形式 | 毫秒级工夫 | 序列号 | 机器 ID |
---|---|---|---|---|---|---|
位数 | 63 | 62 | 60-61 | 20-59 | 10-19 | 0-9 |
最大峰值型可能接受更大的峰值压力,然而粗略有序的粒度有点大,最小粒度型有较粗疏的粒度,然而每个毫秒能接受的实践峰值无限,为 1024,同一个毫秒如果有更多的申请产生,必须等到下一个毫秒再响应。
分布式序列号(分布式 ID)的类型在配置时指定,须要重启服务能力相互切换。
数据结构
1. 序列号
最大峰值型
20 位,实践上每秒内均匀可产生 2^20= 1048576 个 ID,百万级别,如果零碎的网络 IO 和 CPU 足够弱小,可接受的峰值达到每毫秒百万级别。
最小粒度型
10 位,每毫秒内序列号总计 2^10=1024 个, 也就是每个毫秒最多产生 1000+ 个 ID,实践上接受的峰值齐全不如咱们最大峰值计划。
2. 秒级工夫 / 毫秒级工夫
最大峰值型
30 位,示意秒级工夫,2^30/60/60/24/365=34,也就是可应用 30+ 年。
最小粒度型
40 位,示意毫秒级工夫,2^40/1000/60/60/24/365=34,同样能够应用 30+ 年。
3. 机器 ID
10 位,2^10=1024, 也就是最多反对 1000+ 个服务器。核心公布模式和 REST 公布模式个别不会有太多数量的机器,依照设计每台机器 TPS 1 万 /s,10 台服务器就能够有 10 万 / s 的 TPS,根本能够满足大部分的业务需要。
然而思考到咱们在业务服务能够应用内嵌公布形式,对机器 ID 的需求量变得更大,这里最多反对 1024 个服务器。
4. 生成形式
2 位,用来辨别三种公布模式:嵌入公布模式,RPC 公布模式,REST 公布模式。
00:嵌入公布模式
01:RPC 公布模式
02:REST 公布模式
03:保留未用
5. 序列号类型
1 位,用来辨别两种 ID 类型:最大峰值型和最小粒度型。
0:最大峰值型
1:最小粒度型
6. 版本
1 位,用来做扩大位或者扩容时候的长期计划。
0:默认值,免得转化为整型再转化回字符串被截断
1:示意扩大或者扩容中
作为 30 年后扩大应用,或者在 30 年后 ID 将近用光之时,扩大为秒级工夫或者毫秒级工夫来挣得零碎的移植工夫窗口,其实只有扩大一位,齐全能够再应用 30 年。
并发解决
对于核心服务器和 REST 公布形式,ID 生成的过程波及到网络 IO 和 CPU 操作,ID 的生成根本都是内存到高速缓存的操作,没有 IO 操作,网络 IO 是零碎的瓶颈。
绝对于 CPU 计算速度来说网络 IO 是瓶颈,因而,ID 产生的服务应用多线程的形式,对于 ID 生成过程中的竞争点 time 和 sequence,这里应用了多种实现形式
- 应用 concurrent 包的 ReentrantLock 进行互斥,这是缺省的实现形式,也是谋求性能和稳固两个指标的斗争计划。
- 应用传统的 synchronized 进行互斥,这种形式的性能略微逊色一些,通过传入 JVM 参数 -Dmykit.serial.sync.lock.impl.key=true 来开启。
- 应用 CAS 形式进行互斥,这种实现形式的性能十分高,然而在高并发环境下 CPU 负载会很高,通过传入 JVM 参数 -Dmykit.serial.atomic.impl.key=true 来开启。
机器 ID 的调配
咱们将机器 ID 分为两个区段,一个区段服务于 RPC 公布模式和 REST 公布模式,另外一个区段服务于嵌入公布模式。
0-923:嵌入公布模式,事后配置,(或者由 Zookeeper 产生),最多反对 924 台内嵌服务器
924 – 1023:核心服务器公布模式和 REST 公布模式,最多反对 300 台,最大反对 300* 1 万 =300 万 / s 的 TPS
如果嵌入式公布模式和 RPC 公布模式以及 REST 公布模式的使用量不合乎这个比例,咱们能够动静调整两个区间的值来适应。
另外,各个垂直业务之间具备天生的隔离性,每个业务都能够应用最多 1024 台服务器。
与 Zookeeper 集成
对于嵌入公布模式,服务启动须要连贯 Zookeeper 集群,Zookeeper 调配一个 0 -923 区间的一个 ID,如果 0 -923 区间的 ID 被用光,Zookeeper 会调配一个大于 923 的 ID,这种状况,回绝启动服务。
如果不想应用 Zookeeper 产生的惟一的机器 ID,咱们提供缺省的预配的机器 ID 解决方案,每个应用对立分布式全局序列号(分布式 ID)服务的服务须要事后配置一个默认的机器 ID。
工夫同步
应用 mykit-serial 生成分布式全局序列号(分布式 ID)时,须要咱们保障服务器的工夫失常。此时,咱们能够应用 Linux 的定时工作 crontab,定时通过授时服务器虚构集群(寰球有 3000 多台服务器)来核准服务器的工夫。
ntpdate -u pool.ntp.orgpool.ntp.org
性能
最终的性能验证要保障每台服务器的 TPS 达到 1 万 / s 以上。
Restful API 文档
产生分布式全局序列号
- 形容:依据零碎工夫产生一个全局惟一的全局序列号并且在办法体内返回。
- 门路:/genSerialNumber
- 参数:N/A
- 非空参数:N/A
- 示例:http://localhost:8080/genSerialNumber
- 后果:3456526092514361344
反解全局序列号
- 形容:对产生的 serialNumber 进行反解,在响应体内返回反解的 JSON 字符串。
- 门路:/expSerialNumber
- 参数:serialNumber=?
- 非空参数:serialNumber
- 示例:http://localhost:8080/expSerialNumber?serialNumber=3456526092514361344
- 后果:{“genMethod”:2,”machine”:1022,”seq”:0,”time”:12758739,”type”:0,”version”:0}
翻译工夫
- 形容:把长整型的工夫转化成可读的格局。
- 门路:/transtime
- 参数:time=?
- 非空参数:time
- 示例:http://localhost:8080/transtime?time=12758739
- 后果:Thu May 28 16:05:39 CST 2015
制作全局序列号
- 形容:通过给定的分布式全局序列号元素制作分布式全局序列号。
- 门路:/makeSerialNumber
- 参数:genMethod=?&machine=?&seq=?&time=?&type=?&version=?
- 非空参数:time,seq
- 示例:http://localhost:8080/makeSerialNumber?genMethod=2&machine=1022&seq=0&time=12758739&type=0&version=0
- 后果:3456526092514361344
Java API 文档
产生全局序列号
- 形容:依据零碎工夫产生一个全局惟一的分布式序列号(分布式 ID)并且在办法体内返回。
- 类:SerialNumberService
- 办法:genSerialNumber
- 参数:N/A
- 返回类型:long
- 示例:long serialNumber= serialNumberService.genSerialNumber();
反解全局序列号
- 形容:对产生的分布式序列号(分布式 ID)进行反解,在响应体内返回反解的 JSON 字符串。
- 类:SerialNumberService
- 办法:expSerialNumber
- 参数:long serialNumber
- 返回类型:SerialNumber
- 示例:SerialNumber serialNumber = serialNumberService.expSerialNumber(3456526092514361344);
翻译工夫
- 形容:把长整型的工夫转化成可读的格局。
- 类:SerialNumberService
- 办法:transTime
- 参数:long time
- 返回类型:Date
- 示例:Date date = serialNumberService.transTime(12758739);
制作全局序列号 (1)
- 形容:通过给定的分布式序列号元素制作分布式序列号。
- 类:SerialNumberService
- 办法:makeSerialNumber
- 参数:long time, long seq
- 返回类型:long
- 示例:long serialNumber= SerialNumberService.makeSerialNumber(12758739, 0);
制作全局序列号 (2)
- 形容:通过给定的 ID 元素制作 ID。
- 类:SerialNumberService
- 办法:makeSerialNumber
- 参数:long machine, long time, long seq
- 返回类型:long
- 示例:long serialNumber= serialNumberService.makeSerialNumber(1, 12758739, 0);
制作全局序列号 (3)
- 形容:通过给定的分布式序列号元素制作 ID。
- 类:SerialNumberService
- 办法:makeSerialNumber
- 参数:long genMethod, long machine, long time, long seq
- 返回类型:long
- 示例:long serialNumber= serialNumberService.makeSerialNumber(0, 1, 12758739, 0);
制作全局序列号(4)
- 形容:通过给定的分布式序列号元素制作 ID。
- 类:SerialNumberService
- 办法:makeSerialNumber
- 参数:long type, long genMethod, long machine, long time, long seq
- 返回类型:long
- 示例:long serialNumber= serialNumberService.makeSerialNumber(0, 2, 1, 12758739, 0);
制作全局序列号 (5)
- 形容:通过给定的 ID 元素制作 ID。
- 类:SerialNumberService
- 办法:makeSerialNumber
- 参数:long version, long type, long genMethod, long machine, long time, long seq
- 返回类型:long
- 示例:long serialNumber = serialNumberService.makeSerialNumber(0, 0, 2, 1, 12758739, 0);
FAQ
1. 调整工夫是否会影响 ID 产生性能?
未重启机器调慢工夫,mykit-serial 抛出异样,回绝产生 ID。重启机器调快工夫,调整后失常产生 ID,调整时段内没有 ID 产生。
2. 重启调慢或调快工夫有何影响?
重启机器调慢工夫,mykit-serial 将可能产生反复的工夫,系统管理员须要保障不会产生这种状况。重启机器调快工夫,调整后失常产生 ID,调整时段内没有 ID 产生。
3. 每 4 年一次同步润秒会不会影响 ID 产生性能?
原子时钟和电子时钟每四年误差为 1 秒,也就是说电子时钟每 4 年会比原子时钟慢 1 秒,所以,每隔四年,网络时钟都会同步一次工夫,然而本地机器 Windows,Linux 等不会主动同步工夫,须要手工同步,或者应用 ntpupdate 向网络时钟同步。因为时钟是调快 1 秒,调整后不影响 ID 产生,调整的 1s 内没有 ID 产生。
好了明天就到这儿吧,我是冰河,咱们下期见~~
重磅福利
微信搜一搜【冰河技术】微信公众号,关注这个有深度的程序员,每天浏览超硬核技术干货,公众号内回复【PDF】有我筹备的一线大厂面试材料和我原创的超硬核 PDF 技术文档,以及我为大家精心筹备的多套简历模板(不断更新中),心愿大家都能找到心仪的工作,学习是一条时而郁郁寡欢,时而开怀大笑的路,加油。如果你通过致力胜利进入到了心仪的公司,肯定不要懈怠放松,职场成长和新技术学习一样,逆水行舟。如果有幸咱们江湖再见!
另外,我开源的各个 PDF,后续我都会继续更新和保护,感激大家长期以来对冰河的反对!!