常見分布式全局唯一ID生成策略

全局唯一的 ID 幾乎是所有系統(tǒng)都會遇到的剛需安吁。這個(gè) id 在搜索, 存儲數(shù)據(jù), 加快檢索速度 等等很多方面都有著重要的意義。工業(yè)上有多種策略來獲取這個(gè)全局唯一的id睛琳,針對常見的幾種場景盒蟆,我在這里進(jìn)行簡單的總結(jié)和對比。


簡單分析一下需求 [1]

所謂全局唯一的 id 其實(shí)往往對應(yīng)是生成唯一記錄標(biāo)識的業(yè)務(wù)需求师骗。

這個(gè) id 常常是數(shù)據(jù)庫的主鍵历等,數(shù)據(jù)庫上會建立聚集索引(cluster index),即在物理存儲上以這個(gè)字段排序辟癌。這個(gè)記錄標(biāo)識上的查詢寒屯,往往又有分頁或者排序的業(yè)務(wù)需求。所以往往要有一個(gè)time字段黍少,并且在time字段上建立普通索引(non-cluster index)浩螺。普通索引存儲的是實(shí)際記錄的指針,其訪問效率會比聚集索引慢仍侥,如果記錄標(biāo)識在生成時(shí)能夠基本按照時(shí)間有序要出,則可以省去這個(gè)time字段的索引查詢。

這就引出了記錄標(biāo)識生成的兩大核心需求:

全局唯一

趨勢有序

常見生成策略的優(yōu)缺點(diǎn)對比 [2]

方法一: 用數(shù)據(jù)庫的 auto_increment 來生成

優(yōu)點(diǎn):

此方法使用數(shù)據(jù)庫原有的功能农渊,所以相對簡單

能夠保證唯一性

能夠保證遞增性

id 之間的步長是固定且可自定義的

缺點(diǎn):

可用性難以保證:數(shù)據(jù)庫常見架構(gòu)是 一主多從 + 讀寫分離患蹂,生成自增ID是寫請求?主庫掛了就玩不轉(zhuǎn)了

擴(kuò)展性差,性能有上限:因?yàn)閷懭胧菃吸c(diǎn)砸紊,數(shù)據(jù)庫主庫的寫性能決定ID的生成性能上限传于,并且?難以擴(kuò)展

改進(jìn)方案:

冗余主庫,避免寫入單點(diǎn)

數(shù)據(jù)水平切分醉顽,保證各主庫生成的ID不重復(fù)



如上圖所述沼溜,由1個(gè)寫庫變成3個(gè)寫庫,每個(gè)寫庫設(shè)置不同的 auto_increment 初始值游添,以及相同的增長步長系草,以保證每個(gè)數(shù)據(jù)庫生成的ID是不同的(上圖中DB 01生成0,3,6,9…,DB 02生成1,4,7,10唆涝,DB 03生成2,5,8,11…)


改進(jìn)后的架構(gòu)保證了可用性找都,但缺點(diǎn)是

喪失了ID生成的“絕對遞增性”:先訪問DB 01生成0,3,再訪問DB 02生成1廊酣,可能導(dǎo)致在非常短的時(shí)間內(nèi)能耻,ID生成不是絕對遞增的(這個(gè)問題不大,目標(biāo)是趨勢遞增,不是絕對遞增

數(shù)據(jù)庫的寫壓力依然很大晓猛,每次生成ID都要訪問數(shù)據(jù)庫


為了解決這些問題饿幅,引出了以下方法:

方法二:單點(diǎn)批量ID生成服務(wù)

分布式系統(tǒng)之所以難,很重要的原因之一是“沒有一個(gè)全局時(shí)鐘戒职,難以保證絕對的時(shí)序”诫睬,要想保證絕對的時(shí)序,還是只能使用單點(diǎn)服務(wù)帕涌,用本地時(shí)鐘保證“絕對時(shí)序”。

數(shù)據(jù)庫寫壓力大续徽,是因?yàn)槊看紊蒊D都訪問了數(shù)據(jù)庫蚓曼,可以使用批量的方式降低數(shù)據(jù)庫寫壓力。



如上圖所述钦扭,數(shù)據(jù)庫使用雙master保證可用性纫版,數(shù)據(jù)庫中只存儲當(dāng)前ID的最大值,例如4客情。

ID生成服務(wù)假設(shè)每次批量拉取5個(gè)ID其弊,服務(wù)訪問數(shù)據(jù)庫,將當(dāng)前ID的最大值修改為4膀斋,這樣應(yīng)用訪問ID生成服務(wù)索要ID梭伐,ID生成服務(wù)不需要每次訪問數(shù)據(jù)庫,就能依次派發(fā)0,1,2,3,4這些ID了仰担。

當(dāng)ID發(fā)完后糊识,再將ID的最大值修改為11,就能再次派發(fā)6,7,8,9,10,11這些ID了摔蓝,于是數(shù)據(jù)庫的壓力就降低到原來的1/6赂苗。

優(yōu)點(diǎn):

保證了ID生成的絕對遞增有序

大大的降低了數(shù)據(jù)庫的壓力,ID生成可以做到每秒生成幾萬幾十萬個(gè)

缺點(diǎn):

服務(wù)仍然是單點(diǎn)

如果服務(wù)掛了贮尉,服務(wù)重啟起來之后拌滋,繼續(xù)生成ID可能會不連續(xù),中間出現(xiàn)空洞(服務(wù)內(nèi)存是保存著0,1,2,3,4猜谚,數(shù)據(jù)庫中max-id是4败砂,分配到3時(shí),服務(wù)重啟了魏铅,下次會從5開始分配吠卷,3和4就成了空洞,不過這個(gè)問題也不大)

雖然每秒可以生成幾萬幾十萬個(gè)ID沦零,但畢竟還是有性能上限祭隔,無法進(jìn)行水平擴(kuò)展

改進(jìn)方案

單點(diǎn)服務(wù)的常用高可用優(yōu)化方案是“備用服務(wù)”,也叫“影子服務(wù)”,所以我們能用以下方法優(yōu)化上述缺點(diǎn):



如上圖疾渴,對外提供的服務(wù)是主服務(wù)千贯,有一個(gè)影子服務(wù)時(shí)刻處于備用狀態(tài),當(dāng)主服務(wù)掛了的時(shí)候影子服務(wù)頂上搞坝。這個(gè)切換的過程對調(diào)用方是透明的搔谴,可以自動完成,常用的技術(shù)是?vip+keepalived桩撮。另外敦第,id generate service 也可以進(jìn)行水平擴(kuò)展,以解決上述缺點(diǎn)店量,但會引發(fā)一致性問題芜果。

方法三:uuid / guid

不管是通過數(shù)據(jù)庫,還是通過服務(wù)來生成ID融师,業(yè)務(wù)方Application都需要進(jìn)行一次遠(yuǎn)程調(diào)用右钾,比較耗時(shí)。uuid是一種常見的本地生成ID的方法旱爆。

UUID uuid = UUID.randomUUID();

優(yōu)點(diǎn):

本地生成ID舀射,不需要進(jìn)行遠(yuǎn)程調(diào)用,時(shí)延低

擴(kuò)展性好怀伦,基本可以認(rèn)為沒有性能上限


缺點(diǎn):

無法保證趨勢遞增

uuid過長脆烟,往往用字符串表示,作為主鍵建立索引查詢效率低房待,常見優(yōu)化方案為“轉(zhuǎn)化為兩個(gè)uint64整數(shù)存儲”或者“折半存儲”(折半后不能保證唯一性)


方法四:取當(dāng)前毫秒數(shù)

uuid是一個(gè)本地算法浩淘,生成性能高,但無法保證趨勢遞增吴攒,且作為字符串ID檢索效率低张抄,有沒有一種能保證遞增的本地算法呢? - 取當(dāng)前毫秒數(shù)是一種常見方案洼怔。

優(yōu)點(diǎn):

本地生成ID署惯,不需要進(jìn)行遠(yuǎn)程調(diào)用,時(shí)延低

生成的ID趨勢遞增

生成的ID是整數(shù)镣隶,建立索引后查詢效率高

缺點(diǎn):

如果并發(fā)量超過1000极谊,會生成重復(fù)的ID

這個(gè)缺點(diǎn)要了命了,不能保證ID的唯一性安岂。當(dāng)然轻猖,使用微秒可以降低沖突概率,但每秒最多只能生成1000000個(gè)ID域那,再多的話就一定會沖突了咙边,所以使用微秒并不從根本上解決問題。

方法五:使用 Redis 來生成 id

當(dāng)使用數(shù)據(jù)庫來生成ID性能不夠要求的時(shí)候,我們可以嘗試使用Redis來生成ID败许。這主要依賴于Redis是單線程的王带,所以也可以用生成全局唯一的ID∈幸螅可以用Redis的原子操作?INCR和?INCRBY?來實(shí)現(xiàn)愕撰。

優(yōu)點(diǎn):

依賴于數(shù)據(jù)庫,靈活方便醋寝,且性能優(yōu)于數(shù)據(jù)庫搞挣。

數(shù)字ID天然排序,對分頁或者需要排序的結(jié)果很有幫助音羞。

缺點(diǎn):

如果系統(tǒng)中沒有Redis囱桨,還需要引入新的組件,增加系統(tǒng)復(fù)雜度黄选。

需要編碼和配置的工作量比較大。


方法六:Twitter 開源的 Snowflake 算法

snowflake 是 twitter 開源的分布式ID生成算法婶肩,其核心思想為办陷,一個(gè)long型的ID:

41 bit 作為毫秒數(shù) -?41位的長度可以使用69年

10 bit 作為機(jī)器編號 (5個(gè)bit是數(shù)據(jù)中心,5個(gè)bit的機(jī)器ID) -?10位的長度最多支持部署1024個(gè)節(jié)點(diǎn)

12 bit 作為毫秒內(nèi)序列號 -?12位的計(jì)數(shù)順序號支持每個(gè)節(jié)點(diǎn)每毫秒產(chǎn)生4096個(gè)ID序號



算法單機(jī)每秒內(nèi)理論上最多可以生成1000*(2^12)律歼,也就是400W的ID民镜,完全能滿足業(yè)務(wù)的需求。

該算法 java 版本的實(shí)現(xiàn)代碼如下:

public class SnowflakeIdGenerator {

//================================================Algorithm's Parameter=============================================

// 系統(tǒng)開始時(shí)間截 (UTC 2017-06-28 00:00:00)

private final long startTime = 1498608000000L;

// 機(jī)器id所占的位數(shù)

private final long workerIdBits = 5L;

// 數(shù)據(jù)標(biāo)識id所占的位數(shù)

private final long dataCenterIdBits = 5L;

// 支持的最大機(jī)器id(十進(jìn)制)险毁,結(jié)果是31 (這個(gè)移位算法可以很快的計(jì)算出幾位二進(jìn)制數(shù)所能表示的最大十進(jìn)制數(shù))

// -1L 左移 5位 (worker id 所占位數(shù)) 即 5位二進(jìn)制所能獲得的最大十進(jìn)制數(shù) - 31

private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

// 支持的最大數(shù)據(jù)標(biāo)識id - 31

private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);

// 序列在id中占的位數(shù)

private final long sequenceBits = 12L;

// 機(jī)器ID 左移位數(shù) - 12 (即末 sequence 所占用的位數(shù))

private final long workerIdMoveBits = sequenceBits;

// 數(shù)據(jù)標(biāo)識id 左移位數(shù) - 17(12+5)

private final long dataCenterIdMoveBits = sequenceBits + workerIdBits;

// 時(shí)間截向 左移位數(shù) - 22(5+5+12)

private final long timestampMoveBits = sequenceBits + workerIdBits + dataCenterIdBits;

// 生成序列的掩碼(12位所對應(yīng)的最大整數(shù)值)制圈,這里為4095 (0b111111111111=0xfff=4095)

private final long sequenceMask = -1L ^ (-1L << sequenceBits);

//=================================================Works's Parameter================================================

/**

* 工作機(jī)器ID(0~31)

*/

private long workerId;

/**

* 數(shù)據(jù)中心ID(0~31)

*/

private long dataCenterId;

/**

* 毫秒內(nèi)序列(0~4095)

*/

private long sequence = 0L;

/**

* 上次生成ID的時(shí)間截

*/

private long lastTimestamp = -1L;

//===============================================Constructors=======================================================

/**

* 構(gòu)造函數(shù)

*

* @param workerId 工作ID (0~31)

* @param dataCenterId 數(shù)據(jù)中心ID (0~31)

*/

public SnowflakeIdGenerator(long workerId, long dataCenterId) {

if (workerId > maxWorkerId || workerId < 0) {

throw new IllegalArgumentException(String.format("Worker Id can't be greater than %d or less than 0", maxWorkerId));

}

if (dataCenterId > maxDataCenterId || dataCenterId < 0) {

throw new IllegalArgumentException(String.format("DataCenter Id can't be greater than %d or less than 0", maxDataCenterId));

}

this.workerId = workerId;

this.dataCenterId = dataCenterId;

}

// ==================================================Methods========================================================

// 線程安全的獲得下一個(gè) ID 的方法

public synchronized long nextId() {

long timestamp = currentTime();

//如果當(dāng)前時(shí)間小于上一次ID生成的時(shí)間戳: 說明系統(tǒng)時(shí)鐘回退過 - 這個(gè)時(shí)候應(yīng)當(dāng)拋出異常

if (timestamp < lastTimestamp) {

throw new RuntimeException(

String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));

}

//如果是同一時(shí)間生成的,則進(jìn)行毫秒內(nèi)序列

if (lastTimestamp == timestamp) {

sequence = (sequence + 1) & sequenceMask;

//毫秒內(nèi)序列溢出 即 序列 > 4095

if (sequence == 0) {

//阻塞到下一個(gè)毫秒,獲得新的時(shí)間戳

timestamp = blockTillNextMillis(lastTimestamp);

}

}

//時(shí)間戳改變畔况,毫秒內(nèi)序列重置

else {

sequence = 0L;

}

//上次生成ID的時(shí)間截

lastTimestamp = timestamp;

//移位并通過或運(yùn)算拼到一起組成64位的ID

return ((timestamp - startTime) << timestampMoveBits) //

| (dataCenterId << dataCenterIdMoveBits) //

| (workerId << workerIdMoveBits) //

| sequence;

}

// 阻塞到下一個(gè)毫秒 即 直到獲得新的時(shí)間戳

protected long blockTillNextMillis(long lastTimestamp) {

long timestamp = currentTime();

while (timestamp <= lastTimestamp) {

timestamp = currentTime();

}

return timestamp;

}

// 獲得以毫秒為單位的當(dāng)前時(shí)間

protected long currentTime() {

return System.currentTimeMillis();

}

//====================================================Test Case=====================================================

public static void main(String[] args) {

SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(0, 0);

for (int i = 0; i < 1000; i++) {

long id = idWorker.nextId();

System.out.println(Long.toBinaryString(id));

System.out.println(id);

}

}

}

如果想學(xué)習(xí)Java工程化鲸鹦、高性能及分布式、深入淺出跷跪。性能調(diào)優(yōu)馋嗜、Spring,MyBatis吵瞻,Netty源碼分析的朋友可以加我的Java高級架構(gòu)進(jìn)階群:180705916葛菇,群里有阿里大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費(fèi)分享給大家

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末橡羞,一起剝皮案震驚了整個(gè)濱河市眯停,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌卿泽,老刑警劉巖莺债,帶你破解...
    沈念sama閱讀 216,651評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡九府,警方通過查閱死者的電腦和手機(jī)椎瘟,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,468評論 3 392
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來侄旬,“玉大人肺蔚,你說我怎么就攤上這事±芨幔” “怎么了宣羊?”我有些...
    開封第一講書人閱讀 162,931評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長汰蜘。 經(jīng)常有香客問我仇冯,道長,這世上最難降的妖魔是什么族操? 我笑而不...
    開封第一講書人閱讀 58,218評論 1 292
  • 正文 為了忘掉前任苛坚,我火速辦了婚禮,結(jié)果婚禮上色难,老公的妹妹穿的比我還像新娘泼舱。我一直安慰自己,他們只是感情好枷莉,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,234評論 6 388
  • 文/花漫 我一把揭開白布娇昙。 她就那樣靜靜地躺著,像睡著了一般笤妙。 火紅的嫁衣襯著肌膚如雪冒掌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,198評論 1 299
  • 那天蹲盘,我揣著相機(jī)與錄音股毫,去河邊找鬼。 笑死召衔,一個(gè)胖子當(dāng)著我的面吹牛皇拣,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播薄嫡,決...
    沈念sama閱讀 40,084評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼氧急,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了毫深?” 一聲冷哼從身側(cè)響起吩坝,我...
    開封第一講書人閱讀 38,926評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎哑蔫,沒想到半個(gè)月后钉寝,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體弧呐,經(jīng)...
    沈念sama閱讀 45,341評論 1 311
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,563評論 2 333
  • 正文 我和宋清朗相戀三年嵌纲,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了俘枫。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,731評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡逮走,死狀恐怖鸠蚪,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情师溅,我是刑警寧澤茅信,帶...
    沈念sama閱讀 35,430評論 5 343
  • 正文 年R本政府宣布,位于F島的核電站墓臭,受9級特大地震影響蘸鲸,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜窿锉,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,036評論 3 326
  • 文/蒙蒙 一酌摇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧嗡载,春花似錦窑多、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,676評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽琳轿。三九已至判沟,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間崭篡,已是汗流浹背挪哄。 一陣腳步聲響...
    開封第一講書人閱讀 32,829評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留琉闪,地道東北人迹炼。 一個(gè)月前我還...
    沈念sama閱讀 47,743評論 2 368
  • 正文 我出身青樓,卻偏偏與公主長得像颠毙,于是被迫代替她去往敵國和親斯入。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,629評論 2 354

推薦閱讀更多精彩內(nèi)容

  • 9月14日蛀蜜,我和他爸爸參加了園里舉辦的“中秋賞月刻两,放花燈”的中秋節(jié)主題活動”。這是我們第一次參加幼兒園舉辦的第一次...
    蔚然咔咔閱讀 554評論 0 0
  • 7月份已經(jīng)過去很久了滴某,一直想做總結(jié)磅摹,可是因?yàn)樽约鹤龅牟粔蚝米搪酰惶靡馑甲隹偨Y(jié)。6月份做總結(jié)的時(shí)候?qū)懙?月份計(jì)劃户誓,很...
    張含雪閱讀 323評論 0 3
  • 2017.01.01 立Flag嗎饼灿? 主線是進(jìn)步,進(jìn)步帝美,進(jìn)步碍彭。 方法是學(xué)習(xí)學(xué)習(xí)再學(xué)習(xí)。 做時(shí)間的朋友证舟,走得更遠(yuǎn)硕旗,體...
    iAM秋秋閱讀 485評論 0 51
  • 冬天的頤和園北宮門漆枚,淡淡的不施絲毫脂粉,就像一副水墨畫韻味十足抵知,百看不厭墙基。 今天是冬至,是夜最長晝最短的日子刷喜,往后...
    孔雀東南飛飛閱讀 931評論 10 45