一秀仲,題記
所有的業(yè)務系統(tǒng)融痛,都有生成ID的需求,如訂單id神僵,商品id雁刷,文章ID等。這個ID會是數(shù)據(jù)庫中的唯一主鍵保礼,在它上面會建立聚集索引沛励!
ID生成的核心需求有兩點:
全局唯一
趨勢有序
二责语,為什么要趨勢有序
以mysql為例,InnoDB引擎表是基于B+樹的索引組織表目派;每個表都需要有一個聚集索引(clustered index)坤候;所有的行記錄都存儲在B+樹的葉子節(jié)點(leaf pages of the tree);基于聚集索引的增企蹭、刪白筹、改、查的效率相對是最高的谅摄;如下圖:
如果我們定義了主鍵(PRIMARY KEY)徒河,那么InnoDB會選擇其作為聚集索引;
如果沒有顯式定義主鍵送漠,則InnoDB會選擇第一個不包含有NULL值的唯一索引作為主鍵索引虚青;
如果也沒有這樣的唯一索引,則InnoDB會選擇內置6字節(jié)長的ROWID作為隱含的聚集索引(ROWID隨著行記錄的寫入而主鍵遞增螺男,這個ROWID不像ORACLE的ROWID那樣可引用,是隱含的)纵穿。
綜上總結下隧,如果InnoDB表的數(shù)據(jù)寫入順序能和B+樹索引的葉子節(jié)點順序一致的話,這時候存取效率是最高的谓媒,也就是下面這幾種情況的存取效率最高
使用自增列(INT/BIGINT類型)做主鍵淆院,這時候寫入順序是自增的,和B+數(shù)葉子節(jié)點分裂順序一致句惯;
該表不指定自增列做主鍵土辩,同時也沒有可以被選為主鍵的唯一索引(上面的條件),這時候InnoDB會選擇內置的ROWID作為主鍵抢野,寫入順序和ROWID增長順序一致拷淘;
除此以外,如果一個InnoDB表又沒有顯示主鍵指孤,又有可以被選擇為主鍵的唯一索引启涯,但該唯一索引可能不是遞增關系時(例如字符串、UUID恃轩、多字段聯(lián)合唯一索引的情況)结洼,該表的存取效率就會比較差。)
這就是為什么我們的分布式ID一定要是趨勢遞增的叉跛!那么在開發(fā)當中松忍,面對這種分布式ID需求,常見的處理方案有哪些呢筷厘?
三鸣峭,數(shù)據(jù)庫自增長序列或字段
最常見的方式宏所。利用數(shù)據(jù)庫,全數(shù)據(jù)庫唯一叽掘。
優(yōu)點:
1)簡單楣铁,代碼方便,性能可以接受更扁。
2)數(shù)字ID天然排序盖腕,對分頁或者需要排序的結果很有幫助。
缺點:
1)不同數(shù)據(jù)庫語法和實現(xiàn)不同浓镜,數(shù)據(jù)庫遷移的時候或多數(shù)據(jù)庫版本支持的時候需要處理溃列。
2)在單個數(shù)據(jù)庫或讀寫分離或一主多從的情況下,只有一個主庫可以生成膛薛。有單點故障的風險听隐。
3)在性能達不到要求的情況下,比較難于擴展哄啄。
4)如果遇見多個系統(tǒng)需要合并或者涉及到數(shù)據(jù)遷移會相當痛苦莫辨。
5)分表分庫的時候會有麻煩萎羔。
優(yōu)化方案:
1)針對主庫單點,如果有多個Master庫,則每個Master庫設置的起始數(shù)字不一樣山憨,步長一樣携御,可以是Master的個數(shù)搂抒。比如:Master1 生成的是 1姑宽,4,7刊殉,10殉摔,Master2生成的是2,5,8,11 Master3生成的是 3,6,9,12。這樣就可以有效生成集群中的唯一ID记焊,也可以大大降低ID生成數(shù)據(jù)庫操作的負載逸月。
四,UUID
常見的方式亚亲〕共桑可以利用數(shù)據(jù)庫也可以利用程序生成,一般來說全球唯一捌归。
優(yōu)點:
1)簡單肛响,代碼方便。
2)生成ID性能非常好惜索,基本不會有性能問題特笋。
3)全球唯一,在遇見數(shù)據(jù)遷移,系統(tǒng)數(shù)據(jù)合并猎物,或者數(shù)據(jù)庫變更等情況下虎囚,可以從容應對。
缺點:
1)沒有排序蔫磨,無法保證趨勢遞增淘讥。
2)UUID往往是使用字符串存儲,查詢的效率比較低堤如。
3)存儲空間比較大蒲列,如果是海量數(shù)據(jù)庫,就需要考慮存儲量的問題搀罢。
4)傳輸數(shù)據(jù)量大
5)不可讀蝗岖。
五,Redis生成ID
當使用數(shù)據(jù)庫來生成ID性能不夠要求的時候榔至,我們可以嘗試使用Redis來生成ID抵赢。這主要依賴于Redis是單線程的,所以也可以用生成全局唯一的ID唧取∏穑可以用Redis的原子操作 INCR和INCRBY來實現(xiàn)。
可以使用Redis集群來獲取更高的吞吐量枫弟。假如一個集群中有5臺Redis彩匕。可以初始化每臺Redis的值分別是1,2,3,4,5媒区,然后步長都是5。各個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
這個掸犬,隨便負載到哪個機確定好袜漩,未來很難做修改。但是3-5臺服務器基本能夠滿足器上湾碎,都可以獲得不同的ID宙攻。但是步長和初始值一定需要事先需要了。使用Redis集群也可以方式單點故障的問題介褥。
另外座掘,比較適合使用Redis來生成每天從0開始的流水號。比如訂單號=日期+當日自增長號柔滔∫缗悖可以每天在Redis中生成一個Key,使用INCR進行累加睛廊。
優(yōu)點:
1)不依賴于數(shù)據(jù)庫形真,靈活方便,且性能優(yōu)于數(shù)據(jù)庫超全。
2)數(shù)字ID天然排序咆霜,對分頁或者需要排序的結果很有幫助邓馒。
缺點:
1)如果系統(tǒng)中沒有Redis,還需要引入新的組件蛾坯,增加系統(tǒng)復雜度光酣。
2)需要編碼和配置的工作量比較大。
六脉课,twitter
twitter在把存儲系統(tǒng)從MySQL遷移到Cassandra的過程中由于Cassandra沒有順序ID生成機制救军,于是自己開發(fā)了一套全局唯一ID生成服務:Snowflake。
1 41位的時間序列(精確到毫秒下翎,41位的長度可以使用69年)
2 10位的機器標識(10位的長度最多支持部署1024個節(jié)點)
3 12位的計數(shù)順序號(12位的計數(shù)順序號支持每個節(jié)點每毫秒產(chǎn)生4096個ID序號) 最高位是符號位缤言,始終為0。
優(yōu)點:
高性能视事,低延遲胆萧;獨立的應用;
按時間有序俐东。?
缺點:
需要獨立的開發(fā)和部署跌穗。
強依賴時鐘,如果主機時間回撥,則會造成重復ID,會產(chǎn)生
ID雖然有序,但是不連續(xù)
原理
七,MongoDB的ObjectId
MongoDB的ObjectId和snowflake算法類似虏辫。它設計成輕量型的蚌吸,不同的機器都能用全局唯一的同種方法方便地生成它。MongoDB 從一開始就設計用來作為分布式數(shù)據(jù)庫砌庄,處理多個節(jié)點是一個核心要求羹唠。使其在分片環(huán)境中要容易生成得多。
ObjectId使用12字節(jié)的存儲空間娄昆,其生成方式如下:
|0|1|2|3|4|5|6 |7|8|9|10|11|
|時間戳 |機器ID|PID|計數(shù)器 |
前四個字節(jié)時間戳是從標準紀元開始的時間戳佩微,單位為秒,有如下特性:
1 時間戳與后邊5個字節(jié)一塊萌焰,保證秒級別的唯一性哺眯;
2 保證插入順序大致按時間排序;
3 隱含了文檔創(chuàng)建時間扒俯;
4 時間戳的實際值并不重要奶卓,不需要對服務器之間的時間進行同步(因為加上機器ID和進程ID已保證此值唯一,唯一性是ObjectId的最終訴求)撼玄。
機器ID是服務器主機標識夺姑,通常是機器主機名的散列值。
同一臺機器上可以運行多個mongod實例掌猛,因此也需要加入進程標識符PID瑟幕。
前9個字節(jié)保證了同一秒鐘不同機器不同進程產(chǎn)生的ObjectId的唯一性。后三個字節(jié)是一個自動增加的計數(shù)器(一個mongod進程需要一個全局的計數(shù)器),保證同一秒的ObjectId是唯一的只盹。同一秒鐘最多允許每個進程擁有(256^3 = 16777216)個不同的ObjectId辣往。
總結一下:時間戳保證秒級唯一,機器ID保證設計時考慮分布式殖卑,避免時鐘同步站削,PID保證同一臺服務器運行多個mongod實例時的唯一性,最后的計數(shù)器保證同一秒內的唯一性(選用幾個字節(jié)既要考慮存儲的經(jīng)濟性孵稽,也要考慮并發(fā)性能的上限)许起。
"_id"既可以在服務器端生成也可以在客戶端生成,在客戶端生成可以降低服務器端的壓力菩鲜。
八园细,類snowflake算法
國內有很多廠家基于snowflake算法進行了國產(chǎn)化,例如
百度的uid-generator:
https://github.com/baidu/uid-generator
美團Leaf:
https://github.com/zhuzhong/idleaf
基本是對snowflake的進一步優(yōu)化接校,比如解決時鐘 回撥問題猛频!
九,總結
總體而言蛛勉,分布式唯一ID需要滿足以下條件:
高可用性:不能有單點故障鹿寻。
全局唯一性:不能出現(xiàn)重復的ID號,既然是唯一標識诽凌,這是最基本的要求毡熏。
趨勢遞增:在MySQL InnoDB引擎中使用的是聚集索引,由于多數(shù)RDBMS使用B-tree的數(shù)據(jù)結構來存儲索引數(shù)據(jù)侣诵,在主鍵的選擇上面我們應該盡量使用有序的主鍵保證寫入性能痢法。
時間有序:以時間為序,或者ID里包含時間杜顺。這樣一是可以少一個索引疯暑,二是冷熱數(shù)據(jù)容易分離。
分片支持:可以控制ShardingId哑舒。比如某一個用戶的文章要放在同一個分片內,這樣查詢效率高幻馁,修改也容易洗鸵。
單調遞增:保證下一個ID一定大于上一個ID,例如事務版本號仗嗦、IM增量消息膘滨、排序等特殊需求。
長度適中:不要太長稀拐,最好64bit火邓。使用long比較好操作,如果是96bit,那就要各種移位相當?shù)牟环奖悴桑€有可能有些組件不能支持這么大的ID躲胳。
信息安全:如果ID是連續(xù)的,惡意用戶的扒取工作就非常容易做了纤勒,直接按照順序下載指定URL即可坯苹;如果是訂單號就更危險了,競爭對手可以直接知道我們一天的單量摇天。所以在一些應用場景下粹湃,會需要ID無規(guī)則、不規(guī)則泉坐。
筆者結合公司實際業(yè)務場景采用redis+lua實現(xiàn)分布式ID生產(chǎn)为鳄,參考:https://github.com/Johor03/redis_generator_id