寫在前面
mykit-serial框架的設(shè)計(jì)參考了李艷鵬大佬開(kāi)源的vesta框架,并徹底重構(gòu)了vesta框架,借鑒了雪花算法(SnowFlake)的思想,并在此基礎(chǔ)上進(jìn)行了全面升級(jí)和優(yōu)化。支持嵌入式(Jar包)、RPC(Dubbo,motan、sofa、SpringCloud、SpringCloud Alibaba等主流的RPC框架)、Restful API(支持SpringBoot和Netty),可支持最大峰值型和最小粒度型兩種模式。
開(kāi)源地址:
為何不用數(shù)據(jù)庫(kù)自增字段?
如果在業(yè)務(wù)系統(tǒng)中使用數(shù)據(jù)庫(kù)的自增字段,自增字段完全依賴于數(shù)據(jù)庫(kù),這在數(shù)據(jù)庫(kù)移植,擴(kuò)容,清洗數(shù)據(jù),分庫(kù)分表等操作時(shí)帶來(lái)很多麻煩。
在數(shù)據(jù)庫(kù)分庫(kù)分表時(shí),有一種辦法是通過(guò)調(diào)整自增字段或者數(shù)據(jù)庫(kù)sequence的步長(zhǎng)來(lái)達(dá)到跨數(shù)據(jù)庫(kù)的ID的唯一性,但仍然是一種強(qiáng)依賴數(shù)據(jù)庫(kù)的解決方案,有諸多的限制,并且強(qiáng)依賴數(shù)據(jù)庫(kù)類型,如果我們想增加一個(gè)數(shù)據(jù)庫(kù)實(shí)例或者將業(yè)務(wù)遷移到一種不同類型的數(shù)據(jù)庫(kù)上,那是相當(dāng)麻煩的。
為什么不用UUID?
UUID雖然能夠保證ID的唯一性,但是,它無(wú)法滿足業(yè)務(wù)系統(tǒng)需要的很多其他特性,例如:時(shí)間粗略有序性,可反解和可制造型。另外,UUID產(chǎn)生的時(shí)候使用完全的時(shí)間數(shù)據(jù),性能比較差,并且UUID比較長(zhǎng),占用空間大,間接導(dǎo)致數(shù)據(jù)庫(kù)性能下降,更重要的是,UUID并不具有有序性,這就導(dǎo)致B+樹(shù)索引在寫的時(shí)候會(huì)有過(guò)多的隨機(jī)寫操作(連續(xù)的ID會(huì)產(chǎn)生部分順序?qū)懀?,另外寫的時(shí)候由于不能產(chǎn)生順序的append操作,需要進(jìn)行insert操作,這會(huì)讀取整個(gè)B+樹(shù)節(jié)點(diǎn)到內(nèi)存,然后插入這條記錄后再將整個(gè)節(jié)點(diǎn)寫回磁盤,這種操作在記錄占用空間比較大的情況下,性能下降比較大。所以,不建議使用UUID。
需要考慮的問(wèn)題
既然數(shù)據(jù)庫(kù)自增ID和UUID有諸多的限制,我們就需要考慮如何設(shè)計(jì)一款分布式全局唯一的序列號(hào)(分布式ID)服務(wù)。這里,我們需要考慮如下一些因素。

全局唯一
分布式系統(tǒng)保證全局唯一的一個(gè)悲觀策略是使用鎖或者分布式鎖,但是,只要使用了鎖,就會(huì)大大的降低性能。
因此,我們可以借鑒Twitter的SnowFlake算法,利用時(shí)間的有序性,并且在時(shí)間的某個(gè)單元下采用自增序列,達(dá)到全局的唯一性。
粗略有序
UUID的最大問(wèn)題就是無(wú)序的,任何業(yè)務(wù)都希望生成的ID是有序的,但是,分布式系統(tǒng)中要做到完全有序,就涉及到數(shù)據(jù)的匯聚,需要用到鎖或者分布式鎖,考慮到效率,需要采用折中的方案,粗略有序。目前有兩種主流的方案,一種是秒級(jí)有序,一種是毫秒級(jí)有序,這里又有一個(gè)權(quán)衡和取舍,我們決定支持兩種方式,通過(guò)配置來(lái)決定服務(wù)使用其中的一種方式。
可反解
一個(gè) ID 生成之后,ID本身帶有很多信息量,線上排查的時(shí)候,我們通常首先看到的是ID,如果根據(jù)ID就能知道什么時(shí)候產(chǎn)生的,從哪里來(lái)的,這樣一個(gè)可反解的 ID 可以幫上很多忙。
如果ID 里有了時(shí)間而且能反解,在存儲(chǔ)層面就會(huì)省下很多傳統(tǒng)的timestamp 一類的字段所占用的空間了,這也是一舉兩得的設(shè)計(jì)。
可制造
一個(gè)系統(tǒng)即使再高可用也不會(huì)保證永遠(yuǎn)不出問(wèn)題,出了問(wèn)題怎么辦,手工處理,數(shù)據(jù)被污染怎么辦,洗數(shù)據(jù),可是手工處理或者洗數(shù)據(jù)的時(shí)候,假如使用數(shù)據(jù)庫(kù)自增字段,ID已經(jīng)被后來(lái)的業(yè)務(wù)覆蓋了,怎么恢復(fù)到系統(tǒng)出問(wèn)題的時(shí)間窗口呢?
所以,我們使用的分布式全局序列號(hào)(分布式ID)服務(wù)一定要可復(fù)制,可恢復(fù) ,可制造。
高性能
不管哪個(gè)業(yè)務(wù),訂單也好,商品也好,如果有新記錄插入,那一定是業(yè)務(wù)的核心功能,對(duì)性能的要求非常高,ID生成取決于網(wǎng)絡(luò)IO和CPU的性能,CPU一般不是瓶頸,根據(jù)經(jīng)驗(yàn),單臺(tái)機(jī)器TPS應(yīng)該達(dá)到10000/s。
高可用
首先,分布式全局序列號(hào)(分布式ID)服務(wù)必須是一個(gè)對(duì)等的集群,一臺(tái)機(jī)器掛掉,請(qǐng)求必須能夠轉(zhuǎn)發(fā)到其他機(jī)器,另外,重試機(jī)制也是必不可少的。最后,如果遠(yuǎn)程服務(wù)宕機(jī),我們需要有本地的容錯(cuò)方案,本地庫(kù)的依賴方式可以作為高可用的最后一道屏障。
也就是說(shuō),我們支持RPC發(fā)布模式,嵌入式發(fā)布模式和REST發(fā)布模式,如果某種模式不可用,可以回退到其他發(fā)布模式,如果Zookeeper不可用,可以會(huì)退到使用本地預(yù)配的機(jī)器ID。從而達(dá)到服務(wù)的最大可用。
可伸縮
作為一個(gè)分布式系統(tǒng),永遠(yuǎn)都不能忽略的就是業(yè)務(wù)在不斷地增長(zhǎng),業(yè)務(wù)的絕對(duì)容量不是衡量一個(gè)系統(tǒng)的唯一標(biāo)準(zhǔn),要知道業(yè)務(wù)是永遠(yuǎn)增長(zhǎng)的,所以,系統(tǒng)設(shè)計(jì)不但要考慮能承受的絕對(duì)容量,還必須考慮業(yè)務(wù)增長(zhǎng)的速度,系統(tǒng)的水平伸縮是否能滿足業(yè)務(wù)的增長(zhǎng)速度是衡量一個(gè)系統(tǒng)的另一個(gè)重要標(biāo)準(zhǔn)。
設(shè)計(jì)與實(shí)現(xiàn)
整體架構(gòu)設(shè)計(jì)
mykit-serial的整體架構(gòu)圖如下所示。

mykit-serial框架各模塊的含義如下:
mykit-bean:提供統(tǒng)一的bean類封裝和整個(gè)框架使用的常量等信息。
mykit-common:封裝整個(gè)框架通用的工具類。
mykit-config:提供全局配置能力。
mykit-core:整個(gè)框架的核心實(shí)現(xiàn)模塊。
mykit-db:存放數(shù)據(jù)庫(kù)腳本。
mykit-interface:整個(gè)框架的核心抽象接口。
mykit-service:基于Spring實(shí)現(xiàn)的核心功能。
mykit-rpc:以RPC方式對(duì)外提供服服務(wù)(后續(xù)支持Dubbo,motan、sofa、SpringCloud、SpringCloud Alibaba等主流的RPC框架)。
mykit-server:目前實(shí)現(xiàn)了Dubbo方式,后續(xù)遷移到mykit-rpc模塊。
mykit-rest:基于SpringBoot實(shí)現(xiàn)的Rest服務(wù)。
mykit-rest_netty:基于Netty實(shí)現(xiàn)的Rest服務(wù)。
mykit-test:整個(gè)框架的測(cè)試模塊,通過(guò)此模塊可以快速掌握mykit-serial的使用方式。
發(fā)布模式
根據(jù)最終的客戶使用方式,可分為嵌入發(fā)布模式,RPC發(fā)布模式和Rest發(fā)布模式。
嵌入發(fā)布模式:只適用于Java客戶端,提供一個(gè)本地的Jar包,Jar包是嵌入式的原生服務(wù),需要提前配置本地機(jī)器ID(或者服務(wù)啟動(dòng)時(shí),由Zookeeper動(dòng)態(tài)分配唯一的分布式序列號(hào)),但是不依賴于中心服務(wù)器。
RPC發(fā)布模式:只適用于Java客戶端,提供一個(gè)服務(wù)的客戶端Jar包,Java程序像調(diào)用本地API一樣來(lái)調(diào)用,但是依賴于中心的分布式序列號(hào)(分布式ID)產(chǎn)生服務(wù)器。
REST發(fā)布模式:中心服務(wù)器通過(guò)Restful API提供服務(wù),供非Java語(yǔ)言客戶端使用。
發(fā)布模式最后會(huì)記錄在生成的全局序列號(hào)中。
序列號(hào)類型
根據(jù)時(shí)間的位數(shù)和序列號(hào)的位數(shù),可分為最大峰值型和最小粒度型。
1. 最大峰值型:采用秒級(jí)有序,秒級(jí)時(shí)間占用30位,序列號(hào)占用20位
| 字段 | 版本 | 類型 | 生成方式 | 秒級(jí)時(shí)間 | 序列號(hào) | 機(jī)器ID |
|---|---|---|---|---|---|---|
| 位數(shù) | 63 | 62 | 60-61 | 30-59 | 10-29 | 0-9 |
2. 最小粒度型:采用毫秒級(jí)有序,毫秒級(jí)時(shí)間占用40位,序列號(hào)占用10位
| 字段 | 版本 | 類型 | 生成方式 | 毫秒級(jí)時(shí)間 | 序列號(hào) | 機(jī)器ID |
|---|---|---|---|---|---|---|
| 位數(shù) | 63 | 62 | 60-61 | 20-59 | 10-19 | 0-9 |
最大峰值型能夠承受更大的峰值壓力,但是粗略有序的粒度有點(diǎn)大,最小粒度型有較細(xì)致的粒度,但是每個(gè)毫秒能承受的理論峰值有限,為1024,同一個(gè)毫秒如果有更多的請(qǐng)求產(chǎn)生,必須等到下一個(gè)毫秒再響應(yīng)。
分布式序列號(hào)(分布式ID)的類型在配置時(shí)指定,需要重啟服務(wù)才能互相切換。
數(shù)據(jù)結(jié)構(gòu)
1. 序列號(hào)
最大峰值型
20位,理論上每秒內(nèi)平均可產(chǎn)生2^20= 1048576個(gè)ID,百萬(wàn)級(jí)別,如果系統(tǒng)的網(wǎng)絡(luò)IO和CPU足夠強(qiáng)大,可承受的峰值達(dá)到每毫秒百萬(wàn)級(jí)別。
最小粒度型
10位,每毫秒內(nèi)序列號(hào)總計(jì)2^10=1024個(gè), 也就是每個(gè)毫秒最多產(chǎn)生1000+個(gè)ID,理論上承受的峰值完全不如我們最大峰值方案。
2. 秒級(jí)時(shí)間/毫秒級(jí)時(shí)間
最大峰值型
30位,表示秒級(jí)時(shí)間,2^30/60/60/24/365=34,也就是可使用30+年。
最小粒度型
40位,表示毫秒級(jí)時(shí)間,2^40/1000/60/60/24/365=34,同樣可以使用30+年。
3. 機(jī)器ID
10位, 2^10=1024, 也就是最多支持1000+個(gè)服務(wù)器。中心發(fā)布模式和REST發(fā)布模式一般不會(huì)有太多數(shù)量的機(jī)器,按照設(shè)計(jì)每臺(tái)機(jī)器TPS 1萬(wàn)/s,10臺(tái)服務(wù)器就可以有10萬(wàn)/s的TPS,基本可以滿足大部分的業(yè)務(wù)需求。
但是考慮到我們?cè)跇I(yè)務(wù)服務(wù)可以使用內(nèi)嵌發(fā)布方式,對(duì)機(jī)器ID的需求量變得更大,這里最多支持1024個(gè)服務(wù)器。
4. 生成方式
2位,用來(lái)區(qū)分三種發(fā)布模式:嵌入發(fā)布模式,RPC發(fā)布模式,REST發(fā)布模式。
00:嵌入發(fā)布模式 01:RPC發(fā)布模式 02:REST發(fā)布模式 03:保留未用
5. 序列號(hào)類型
1位,用來(lái)區(qū)分兩種ID類型:最大峰值型和最小粒度型。
0:最大峰值型 1:最小粒度型
6. 版本
1位,用來(lái)做擴(kuò)展位或者擴(kuò)容時(shí)候的臨時(shí)方案。
0:默認(rèn)值,以免轉(zhuǎn)化為整型再轉(zhuǎn)化回字符串被截?cái)?1:表示擴(kuò)展或者擴(kuò)容中
作為30年后擴(kuò)展使用,或者在30年后ID將近用光之時(shí),擴(kuò)展為秒級(jí)時(shí)間或者毫秒級(jí)時(shí)間來(lái)掙得系統(tǒng)的移植時(shí)間窗口,其實(shí)只要擴(kuò)展一位,完全可以再使用30年。
并發(fā)處理
對(duì)于中心服務(wù)器和REST發(fā)布方式,ID生成的過(guò)程涉及到網(wǎng)絡(luò)IO和CPU操作,ID的生成基本都是內(nèi)存到高速緩存的操作,沒(méi)有IO操作,網(wǎng)絡(luò)IO是系統(tǒng)的瓶頸。
相對(duì)于CPU計(jì)算速度來(lái)說(shuō)網(wǎng)絡(luò)IO是瓶頸,因此,ID產(chǎn)生的服務(wù)使用多線程的方式,對(duì)于ID生成過(guò)程中的競(jìng)爭(zhēng)點(diǎn)time和sequence,這里使用了多種實(shí)現(xiàn)方式
- 使用concurrent包的ReentrantLock進(jìn)行互斥,這是缺省的實(shí)現(xiàn)方式,也是追求性能和穩(wěn)定兩個(gè)目標(biāo)的妥協(xié)方案。
- 使用傳統(tǒng)的synchronized進(jìn)行互斥,這種方式的性能稍微遜色一些,通過(guò)傳入JVM參數(shù)-Dmykit.serial.sync.lock.impl.key=true來(lái)開(kāi)啟。
- 使用CAS方式進(jìn)行互斥,這種實(shí)現(xiàn)方式的性能非常高,但是在高并發(fā)環(huán)境下CPU負(fù)載會(huì)很高,通過(guò)傳入JVM參數(shù)-Dmykit.serial.atomic.impl.key=true來(lái)開(kāi)啟。
機(jī)器ID的分配
我們將機(jī)器ID分為兩個(gè)區(qū)段,一個(gè)區(qū)段服務(wù)于RPC發(fā)布模式和REST發(fā)布模式,另外一個(gè)區(qū)段服務(wù)于嵌入發(fā)布模式。
0-923:嵌入發(fā)布模式,預(yù)先配置,(或者由Zookeeper產(chǎn)生),最多支持924臺(tái)內(nèi)嵌服務(wù)器 924 – 1023:中心服務(wù)器發(fā)布模式和REST發(fā)布模式,最多支持300臺(tái),最大支持300*1萬(wàn)=300萬(wàn)/s的TPS
如果嵌入式發(fā)布模式和RPC發(fā)布模式以及REST發(fā)布模式的使用量不符合這個(gè)比例,我們可以動(dòng)態(tài)調(diào)整兩個(gè)區(qū)間的值來(lái)適應(yīng)。
另外,各個(gè)垂直業(yè)務(wù)之間具有天生的隔離性,每個(gè)業(yè)務(wù)都可以使用最多1024臺(tái)服務(wù)器。
與Zookeeper集成
對(duì)于嵌入發(fā)布模式,服務(wù)啟動(dòng)需要連接Zookeeper集群,Zookeeper分配一個(gè)0-923區(qū)間的一個(gè)ID,如果0-923區(qū)間的ID被用光,Zookeeper會(huì)分配一個(gè)大于923的ID,這種情況,拒絕啟動(dòng)服務(wù)。
如果不想使用Zookeeper產(chǎn)生的唯一的機(jī)器ID,我們提供缺省的預(yù)配的機(jī)器ID解決方案,每個(gè)使用統(tǒng)一分布式全局序列號(hào)(分布式ID)服務(wù)的服務(wù)需要預(yù)先配置一個(gè)默認(rèn)的機(jī)器ID。
時(shí)間同步
使用mykit-serial生成分布式全局序列號(hào)(分布式ID)時(shí),需要我們保證服務(wù)器的時(shí)間正常。此時(shí),我們可以使用Linux的定時(shí)任務(wù)crontab,定時(shí)通過(guò)授時(shí)服務(wù)器虛擬集群(全球有3000多臺(tái)服務(wù)器)來(lái)核準(zhǔn)服務(wù)器的時(shí)間。
ntpdate -u pool.ntp.orgpool.ntp.org
性能
最終的性能驗(yàn)證要保證每臺(tái)服務(wù)器的TPS達(dá)到1萬(wàn)/s以上。
Restful API文檔
產(chǎn)生分布式全局序列號(hào)
描述:根據(jù)系統(tǒng)時(shí)間產(chǎn)生一個(gè)全局唯一的全局序列號(hào)并且在方法體內(nèi)返回。
路徑:/genSerialNumber
參數(shù):N/A
非空參數(shù):N/A
結(jié)果:3456526092514361344
反解全局序列號(hào)
描述:對(duì)產(chǎn)生的serialNumber進(jìn)行反解,在響應(yīng)體內(nèi)返回反解的JSON字符串。
路徑:/expSerialNumber
參數(shù):serialNumber=?
非空參數(shù):serialNumber
示例:http://localhost:8080/expSerialNumber?serialNumber=3456526092514361344
結(jié)果:{"genMethod":2,"machine":1022,"seq":0,"time":12758739,"type":0,"version":0}
翻譯時(shí)間
描述:把長(zhǎng)整型的時(shí)間轉(zhuǎn)化成可讀的格式。
路徑:/transtime
參數(shù):time=?
非空參數(shù):time
結(jié)果:Thu May 28 16:05:39 CST 2015
制造全局序列號(hào)
描述:通過(guò)給定的分布式全局序列號(hào)元素制造分布式全局序列號(hào)。
路徑:/makeSerialNumber
參數(shù):genMethod=?&machine=?&seq=?&time=?&type=?&version=?
非空參數(shù):time,seq
示例:http://localhost:8080/makeSerialNumber?genMethod=2&machine=1022&seq=0&time=12758739&type=0&version=0
結(jié)果:3456526092514361344
Java API文檔
產(chǎn)生全局序列號(hào)
描述:根據(jù)系統(tǒng)時(shí)間產(chǎn)生一個(gè)全局唯一的分布式序列號(hào)(分布式ID)并且在方法體內(nèi)返回。
類:SerialNumberService
方法:genSerialNumber
參數(shù):N/A
返回類型:long
示例:long serialNumber= serialNumberService.genSerialNumber();
反解全局序列號(hào)
描述:對(duì)產(chǎn)生的分布式序列號(hào)(分布式ID)進(jìn)行反解,在響應(yīng)體內(nèi)返回反解的JSON字符串。
類:SerialNumberService
方法:expSerialNumber
參數(shù):long serialNumber
返回類型:SerialNumber
示例:SerialNumber serialNumber = serialNumberService.expSerialNumber(3456526092514361344);
翻譯時(shí)間
描述:把長(zhǎng)整型的時(shí)間轉(zhuǎn)化成可讀的格式。
類:SerialNumberService
方法:transTime
參數(shù):long time
返回類型:Date
示例:Date date = serialNumberService.transTime(12758739);
制造全局序列號(hào)(1)
描述:通過(guò)給定的分布式序列號(hào)元素制造分布式序列號(hào)。
類:SerialNumberService
方法:makeSerialNumber
參數(shù):long time, long seq
返回類型:long
示例:long serialNumber= SerialNumberService.makeSerialNumber(12758739, 0);
制造全局序列號(hào)(2)
描述:通過(guò)給定的ID元素制造ID。
類:SerialNumberService
方法:makeSerialNumber
參數(shù):long machine, long time, long seq
返回類型:long
示例:long serialNumber= serialNumberService.makeSerialNumber(1, 12758739, 0);
制造全局序列號(hào)(3)
描述:通過(guò)給定的分布式序列號(hào)元素制造ID。
類:SerialNumberService
方法:makeSerialNumber
參數(shù):long genMethod, long machine, long time, long seq
返回類型:long
示例:long serialNumber= serialNumberService.makeSerialNumber(0, 1, 12758739, 0);
制造全局序列號(hào)(4)
描述:通過(guò)給定的分布式序列號(hào)元素制造ID。
類:SerialNumberService
方法:makeSerialNumber
參數(shù):long type, long genMethod, long machine, long time, long seq
返回類型:long
示例:long serialNumber= serialNumberService.makeSerialNumber(0, 2, 1, 12758739, 0);
制造全局序列號(hào)(5)
描述:通過(guò)給定的ID元素制造ID。
類:SerialNumberService
方法:makeSerialNumber
參數(shù):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.調(diào)整時(shí)間是否會(huì)影響ID產(chǎn)生功能?
未重啟機(jī)器調(diào)慢時(shí)間,mykit-serial拋出異常,拒絕產(chǎn)生ID。重啟機(jī)器調(diào)快時(shí)間,調(diào)整后正常產(chǎn)生ID,調(diào)整時(shí)段內(nèi)沒(méi)有ID產(chǎn)生。
2.重啟調(diào)慢或調(diào)快時(shí)間有何影響?
重啟機(jī)器調(diào)慢時(shí)間,mykit-serial將可能產(chǎn)生重復(fù)的時(shí)間,系統(tǒng)管理員需要保證不會(huì)發(fā)生這種情況。重啟機(jī)器調(diào)快時(shí)間,調(diào)整后正常產(chǎn)生ID,調(diào)整時(shí)段內(nèi)沒(méi)有ID產(chǎn)生。
3.每4年一次同步潤(rùn)秒會(huì)不會(huì)影響ID產(chǎn)生功能?
原子時(shí)鐘和電子時(shí)鐘每四年誤差為1秒,也就是說(shuō)電子時(shí)鐘每4年會(huì)比原子時(shí)鐘慢1秒,所以,每隔四年,網(wǎng)絡(luò)時(shí)鐘都會(huì)同步一次時(shí)間,但是本地機(jī)器Windows,Linux等不會(huì)自動(dòng)同步時(shí)間,需要手工同步,或者使用ntpupdate向網(wǎng)絡(luò)時(shí)鐘同步。由于時(shí)鐘是調(diào)快1秒,調(diào)整后不影響ID產(chǎn)生,調(diào)整的1s內(nèi)沒(méi)有ID產(chǎn)生。
好了今天就到這兒吧,我是冰河,我們下期見(jiàn)~~