一,題記
所有的業(yè)務(wù)系統(tǒng),都有生成ID的需求,如訂單id,商品id,文章ID等。這個(gè)ID會(huì)是數(shù)據(jù)庫中的唯一主鍵,在它上面會(huì)建立聚集索引!
閱讀本文,建議大家已經(jīng)掌握了扎實(shí)的互聯(lián)網(wǎng)技術(shù),可參考:互聯(lián)網(wǎng)技術(shù)清單
ID生成的核心需求有兩點(diǎn):
全局唯一
趨勢(shì)有序
二,為什么要全局唯一?
著名的例子就是身份證號(hào)碼,身份證號(hào)碼確實(shí)是對(duì)人唯一的,然而一個(gè)人是可以辦理多個(gè)身份證的,例如你身份證丟了,又重新補(bǔ)辦了一張,號(hào)碼不變。
問題來了,因?yàn)橄到y(tǒng)是按照身份證號(hào)碼做唯一主鍵的。此時(shí),如果身份證是被盜的情況下,你是沒有辦法在系統(tǒng)里面注銷的,因?yàn)樾屡f2個(gè)身份證的“主鍵”都是身份證號(hào)碼。
也就是說,舊的身份證仍然逍遙在外,完全有效。這個(gè)時(shí)候,還好有一個(gè)身份證有效時(shí)間的東西,只有靠身份證有效期來辨識(shí)了。不過,這就是現(xiàn)在這么多銀行,電信詐騙的由來,撿到一張身份證,去很多銀行,手機(jī),酒店都可以使用!身份證缺乏注銷機(jī)制!
所以,經(jīng)驗(yàn)告訴我們。不要相信自己的直覺,業(yè)務(wù)上所謂的唯一往往都是不靠譜的,經(jīng)不起時(shí)間的考研的。所以需要單獨(dú)設(shè)置一個(gè)和業(yè)務(wù)無關(guān)的主鍵,專業(yè)術(shù)語叫做代理主鍵(surrogate key)。
這也是為什么數(shù)據(jù)庫設(shè)計(jì)范式,唯一主鍵是第一范式!
三,為什么要趨勢(shì)有序
以mysql為例,InnoDB引擎表是基于B+樹的索引組織表(IOT);每個(gè)表都需要有一個(gè)聚集索引(clustered index);所有的行記錄都存儲(chǔ)在B+樹的葉子節(jié)點(diǎn)(leaf pages of the tree);基于聚集索引的增、刪、改、查的效率相對(duì)是最高的;如下圖:
如果我們定義了主鍵(PRIMARY KEY),那么InnoDB會(huì)選擇其作為聚集索引;
如果沒有顯式定義主鍵,則InnoDB會(huì)選擇第一個(gè)不包含有NULL值的唯一索引作為主鍵索引;
如果也沒有這樣的唯一索引,則InnoDB會(huì)選擇內(nèi)置6字節(jié)長的ROWID作為隱含的聚集索引(ROWID隨著行記錄的寫入而主鍵遞增,這個(gè)ROWID不像ORACLE的ROWID那樣可引用,是隱含的)。
綜上總結(jié),如果InnoDB表的數(shù)據(jù)寫入順序能和B+樹索引的葉子節(jié)點(diǎn)順序一致的話,這時(shí)候存取效率是最高的,也就是下面這幾種情況的存取效率最高
使用自增列(INT/BIGINT類型)做主鍵,這時(shí)候?qū)懭腠樞蚴亲栽龅模虰+數(shù)葉子節(jié)點(diǎn)分裂順序一致;
該表不指定自增列做主鍵,同時(shí)也沒有可以被選為主鍵的唯一索引(上面的條件),這時(shí)候InnoDB會(huì)選擇內(nèi)置的ROWID作為主鍵,寫入順序和ROWID增長順序一致;
除此以外,如果一個(gè)InnoDB表又沒有顯示主鍵,又有可以被選擇為主鍵的唯一索引,但該唯一索引可能不是遞增關(guān)系時(shí)(例如字符串、UUID、多字段聯(lián)合唯一索引的情況),該表的存取效率就會(huì)比較差。)
這就是為什么我們的分布式ID一定要是趨勢(shì)遞增的!那么在開發(fā)當(dāng)中,面對(duì)這種分布式ID需求,常見的處理方案有哪些呢?
四,數(shù)據(jù)庫自增長序列或字段
最常見的方式。利用數(shù)據(jù)庫,全數(shù)據(jù)庫唯一。
優(yōu)點(diǎn):
1)簡單,代碼方便,性能可以接受。
2)數(shù)字ID天然排序,對(duì)分頁或者需要排序的結(jié)果很有幫助。
缺點(diǎn):
1)不同數(shù)據(jù)庫語法和實(shí)現(xiàn)不同,數(shù)據(jù)庫遷移的時(shí)候或多數(shù)據(jù)庫版本支持的時(shí)候需要處理。
2)在單個(gè)數(shù)據(jù)庫或讀寫分離或一主多從的情況下,只有一個(gè)主庫可以生成。有單點(diǎn)故障的風(fēng)險(xiǎn)。
3)在性能達(dá)不到要求的情況下,比較難于擴(kuò)展。
4)如果遇見多個(gè)系統(tǒng)需要合并或者涉及到數(shù)據(jù)遷移會(huì)相當(dāng)痛苦。
5)分表分庫的時(shí)候會(huì)有麻煩。
優(yōu)化方案:
1)針對(duì)主庫單點(diǎn),如果有多個(gè)Master庫,則每個(gè)Master庫設(shè)置的起始數(shù)字不一樣,步長一樣,可以是Master的個(gè)數(shù)。比如:Master1 生成的是 1,4,7,10,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就可以有效生成集群中的唯一ID,也可以大大降低ID生成數(shù)據(jù)庫操作的負(fù)載。
五,UUID
常見的方式。可以利用數(shù)據(jù)庫也可以利用程序生成,一般來說全球唯一。
優(yōu)點(diǎn):
1)簡單,代碼方便。
2)生成ID性能非常好,基本不會(huì)有性能問題。
3)全球唯一,在遇見數(shù)據(jù)遷移,系統(tǒng)數(shù)據(jù)合并,或者數(shù)據(jù)庫變更等情況下,可以從容應(yīng)對(duì)。
缺點(diǎn):
1)沒有排序,無法保證趨勢(shì)遞增。
2)UUID往往是使用字符串存儲(chǔ),查詢的效率比較低。
3)存儲(chǔ)空間比較大,如果是海量數(shù)據(jù)庫,就需要考慮存儲(chǔ)量的問題。
4)傳輸數(shù)據(jù)量大
5)不可讀。
六,Redis生成ID
當(dāng)使用數(shù)據(jù)庫來生成ID性能不夠要求的時(shí)候,我們可以嘗試使用Redis來生成ID。這主要依賴于Redis是單線程的,所以也可以用生成全局唯一的ID。可以用Redis的原子操作 INCR和INCRBY來實(shí)現(xiàn)。
可以使用Redis集群來獲取更高的吞吐量。假如一個(gè)集群中有5臺(tái)Redis。可以初始化每臺(tái)Redis的值分別是1,2,3,4,5,然后步長都是5。各個(gè)Redis生成的ID為:
A:1,6,11,16,21
B:2,7,12,17,22
C:3,8,13,18,23
D:4,9,14,19,24
E:5,10,15,20,25
這個(gè),隨便負(fù)載到哪個(gè)機(jī)確定好,未來很難做修改。但是3-5臺(tái)服務(wù)器基本能夠滿足器上,都可以獲得不同的ID。但是步長和初始值一定需要事先需要了。使用Redis集群也可以方式單點(diǎn)故障的問題。
另外,比較適合使用Redis來生成每天從0開始的流水號(hào)。比如訂單號(hào)=日期+當(dāng)日自增長號(hào)。可以每天在Redis中生成一個(gè)Key,使用INCR進(jìn)行累加。
優(yōu)點(diǎn):
1)不依賴于數(shù)據(jù)庫,靈活方便,且性能優(yōu)于數(shù)據(jù)庫。
2)數(shù)字ID天然排序,對(duì)分頁或者需要排序的結(jié)果很有幫助。
缺點(diǎn):
1)如果系統(tǒng)中沒有Redis,還需要引入新的組件,增加系統(tǒng)復(fù)雜度。
2)需要編碼和配置的工作量比較大。
七,twitter
twitter在把存儲(chǔ)系統(tǒng)從MySQL遷移到Cassandra的過程中由于Cassandra沒有順序ID生成機(jī)制,于是自己開發(fā)了一套全局唯一ID生成服務(wù):Snowflake。
1 41位的時(shí)間序列(精確到毫秒,41位的長度可以使用69年)
2 10位的機(jī)器標(biāo)識(shí)(10位的長度最多支持部署1024個(gè)節(jié)點(diǎn))
3 12位的計(jì)數(shù)順序號(hào)(12位的計(jì)數(shù)順序號(hào)支持每個(gè)節(jié)點(diǎn)每毫秒產(chǎn)生4096個(gè)ID序號(hào)) 最高位是符號(hào)位,始終為0。
優(yōu)點(diǎn):
高性能,低延遲;獨(dú)立的應(yīng)用;
按時(shí)間有序。
缺點(diǎn):
需要獨(dú)立的開發(fā)和部署。
強(qiáng)依賴時(shí)鐘,如果主機(jī)時(shí)間回?fù)?則會(huì)造成重復(fù)ID,會(huì)產(chǎn)生
ID雖然有序,但是不連續(xù)
原理
八,MongoDB的ObjectId
MongoDB的ObjectId和snowflake算法類似。它設(shè)計(jì)成輕量型的,不同的機(jī)器都能用全局唯一的同種方法方便地生成它。MongoDB 從一開始就設(shè)計(jì)用來作為分布式數(shù)據(jù)庫,處理多個(gè)節(jié)點(diǎn)是一個(gè)核心要求。使其在分片環(huán)境中要容易生成得多。
ObjectId使用12字節(jié)的存儲(chǔ)空間,其生成方式如下:
|0|1|2|3|4|5|6 |7|8|9|10|11|
|時(shí)間戳 |機(jī)器ID|PID|計(jì)數(shù)器 |
前四個(gè)字節(jié)時(shí)間戳是從標(biāo)準(zhǔn)紀(jì)元開始的時(shí)間戳,單位為秒,有如下特性:
1 時(shí)間戳與后邊5個(gè)字節(jié)一塊,保證秒級(jí)別的唯一性;
2 保證插入順序大致按時(shí)間排序;
3 隱含了文檔創(chuàng)建時(shí)間;
4 時(shí)間戳的實(shí)際值并不重要,不需要對(duì)服務(wù)器之間的時(shí)間進(jìn)行同步(因?yàn)榧由蠙C(jī)器ID和進(jìn)程ID已保證此值唯一,唯一性是ObjectId的最終訴求)。
機(jī)器ID是服務(wù)器主機(jī)標(biāo)識(shí),通常是機(jī)器主機(jī)名的散列值。
同一臺(tái)機(jī)器上可以運(yùn)行多個(gè)mongod實(shí)例,因此也需要加入進(jìn)程標(biāo)識(shí)符PID。
前9個(gè)字節(jié)保證了同一秒鐘不同機(jī)器不同進(jìn)程產(chǎn)生的ObjectId的唯一性。后三個(gè)字節(jié)是一個(gè)自動(dòng)增加的計(jì)數(shù)器(一個(gè)mongod進(jìn)程需要一個(gè)全局的計(jì)數(shù)器),保證同一秒的ObjectId是唯一的。同一秒鐘最多允許每個(gè)進(jìn)程擁有(256^3 = 16777216)個(gè)不同的ObjectId。
總結(jié)一下:時(shí)間戳保證秒級(jí)唯一,機(jī)器ID保證設(shè)計(jì)時(shí)考慮分布式,避免時(shí)鐘同步,PID保證同一臺(tái)服務(wù)器運(yùn)行多個(gè)mongod實(shí)例時(shí)的唯一性,最后的計(jì)數(shù)器保證同一秒內(nèi)的唯一性(選用幾個(gè)字節(jié)既要考慮存儲(chǔ)的經(jīng)濟(jì)性,也要考慮并發(fā)性能的上限)。
"_id"既可以在服務(wù)器端生成也可以在客戶端生成,在客戶端生成可以降低服務(wù)器端的壓力。
九,類snowflake算法
國內(nèi)有很多廠家基于snowflake算法進(jìn)行了國產(chǎn)化,例如
百度的uid-generator:
https://github.com/baidu/uid-generator
美團(tuán)Leaf:
https://github.com/zhuzhong/idleaf
基本是對(duì)snowflake的進(jìn)一步優(yōu)化,比如解決時(shí)鐘 回?fù)軉栴}!
十,總結(jié)
總體而言,分布式唯一ID需要滿足以下條件:
高可用性:不能有單點(diǎn)故障。
全局唯一性:不能出現(xiàn)重復(fù)的ID號(hào),既然是唯一標(biāo)識(shí),這是最基本的要求。
趨勢(shì)遞增:在MySQL InnoDB引擎中使用的是聚集索引,由于多數(shù)RDBMS使用B-tree的數(shù)據(jù)結(jié)構(gòu)來存儲(chǔ)索引數(shù)據(jù),在主鍵的選擇上面我們應(yīng)該盡量使用有序的主鍵保證寫入性能。
時(shí)間有序:以時(shí)間為序,或者ID里包含時(shí)間。這樣一是可以少一個(gè)索引,二是冷熱數(shù)據(jù)容易分離。
分片支持:可以控制ShardingId。比如某一個(gè)用戶的文章要放在同一個(gè)分片內(nèi),這樣查詢效率高,修改也容易。
單調(diào)遞增:保證下一個(gè)ID一定大于上一個(gè)ID,例如事務(wù)版本號(hào)、IM增量消息、排序等特殊需求。
長度適中:不要太長,最好64bit。使用long比較好操作,如果是96bit,那就要各種移位相當(dāng)?shù)牟环奖悖€有可能有些組件不能支持這么大的ID。
信息安全:如果ID是連續(xù)的,惡意用戶的扒取工作就非常容易做了,直接按照順序下載指定URL即可;如果是訂單號(hào)就更危險(xiǎn)了,競(jìng)爭(zhēng)對(duì)手可以直接知道我們一天的單量。所以在一些應(yīng)用場(chǎng)景下,會(huì)需要ID無規(guī)則、不規(guī)則。