掘金的文章ID是怎麼生成的?
本文已參與「新人創作禮」活動,一起開啟掘金創作之路
引言
在掘金寫了多篇文章之後,我發現了掘金生成文章ID的密碼,先來看看我最近釋出的幾篇文章的連結:
2022-05-18 20:14 http://juejin.cn/post/7099048344558927908
2022-05-17 13:12 http://juejin.cn/post/7098568794745864222
2022-05-16 23:58 http://juejin.cn/post/7098364044238651428
2022-05-15 15:58 http://juejin.cn/post/7097869333308637191
2022-05-14 18:39 http://juejin.cn/post/7097538908270886925
2022-05-13 23:37 http://juejin.cn/post/7097245569776615454
......
首先,我們發現掘金後端的路由簡潔清晰,即/post/{文章ID}
;
其次,這個文章的ID都是19個數字;
最後,ID的數字大小是隨時間遞增的。
有經驗的掘友可能已經想到SnowFlake
了 ^-^~
如果你不清楚,下面請聽我一一道來,本人的一些見解,歡迎大佬評論!
1. 主鍵的選擇
首先,我從資料庫的主鍵選擇說起,主鍵,它可以唯一標識表中的某一條記錄,對資料表來說非常重要。當我們需要查詢和引用表中的一條記錄的時候,最好的辦法就是通過主鍵。
在資料庫設計時建議儘量不要用業務欄位,也就是跟業務有關的欄位做主鍵,因為作為專案設計的技術人員,我們一般無法預測在專案的整個生命週期中,哪個業務欄位會因為專案的業務需求而有重複,或者重用之類的情況出現。
所以你會給表設計一個id作為主鍵,並給這個欄位定義自增約束,如果你是這樣設計的,對於一般的小應用也不會存在大問題。
但是,對與分散式系統,資料庫涉及到多個,就不能依靠資料庫來生成自增的id了,因為同一時間在不同資料庫上可能生成相同的id。為了保證id的唯一性,我們需要把id的生成放到後端的邏輯中。
我之前聽一個老師講,在主資料庫中維護一個資訊表,專門記錄當前id的最大值,然後從資料庫每次在新增id的時候先到主資料庫獲取這個最大值,在這基礎上加一,這個過程需要利用事務防止誤讀。這個方案確實能解決衝突的問題,但是我感覺這個加鎖來保持資料的一致性,可能不能滿足高併發、低延時的情況。
那麼在做資料庫設計之前需要怎麼考慮呢?下面是通用的對ID的要求:
2. 分散式系統對id的要求
-
全域性唯一: 不能出現重複的ID號,既然是唯一標識,這是最基本的要求。
-
趨勢遞增: 在MySQL的InnoDB引擎中使用的是聚集索引,由於多數RDBMS使用BTree的資料結構來儲存索引資料。因此在主鍵的選擇上我們應該儘量使用有序的主鍵保證寫入效能。
-
單調遞增: 保證下一個ID一定大於上一個ID,例如事務版本號,IM增量訊息、排序等特殊需求。
-
資訊保安: 如果ID是連續的,惡意扒取使用者工作就非常容易做了,直接按照順序下載指定的URL即可;如果是訂單號就更危險了,競爭對手可以直接知道我們一天的單量。所以在一些應用場景下,需要ID無規則。
-
含時間戳: 這樣就能夠在開發中快速瞭解分散式ID的生成時間。
-
高可用: 發一個獲取分散式ID的請求,伺服器就可以保證99.999%的情況下給我建立一個唯一的分散式ID
-
低延遲: 發一個獲取分散式ID的請求,伺服器響應速度要快
-
高QPS:假如併發10萬個建立分散式ID請求,伺服器要頂得住並能成功建立10萬個唯一的分散式ID
3. 技術選型
那麼業內有哪些成熟的生成id的方案來滿足上述要求呢?一個是UUID
,另一個是SnowFlake
(雪花演算法)。
3.1 UUID
UUID(Universally Unique Dentifer)
的標準型式包含32個16進位制數字,以連線符分為五段,形式為8-4-4-4-12
的32個字元,示例:630e450a-abcb-435c-cba6-223300000000
優點:
1)簡單,程式碼方便。
2)生成ID效能非常好,基本不會有效能問題。
3)全球唯一,在遇見資料遷移,系統資料合併,或者資料庫變更等情況下,可以從容應對。
UUID效能非常高:本地生成,沒有網路消耗,如果只考慮唯一性UUID是ok的。但是
缺點:
1)沒有排序,無法保證趨勢遞增。無法預測他的生成順序,不能生成遞增有序的數字。
2)分散式id 一般都會作為主鍵, UUID太長,佔用儲存空間比較大,如果是海量資料庫,就需要考慮儲存量的問題。
3)UUID往往是使用字串儲存,查詢的效率比較低。
4)傳輸資料量大,且不可讀。
5)索引, B+樹索引的分裂。既然分散式id是主鍵,主鍵是包含索引的,然後mysql的索引是通過b+樹來實現的, 因為UUID資料是無序的,每一次新的UUID資料的插入,為了查詢的優化,都會對索引"底層的B+樹進行修改,這一點很不好。插入完全無序,不但會導致一些中間節點產生分裂,也會白白創造出很多不飽和的節點,這樣大大降低了資料庫插入的效能。
變種的UUID
1)為了解決UUID不可讀,可以使用UUID to Int64的方法。
2)為了解決UUID無序的問題,NHibernate在其主鍵生成方式中提供了Comb演算法(combined guid/timestamp)。保留GUID的10個位元組,用另6個位元組表示GUID生成的時間(DateTime)
3.2 SnowFlake(雪花演算法)
全域性唯一,並且有序按時間遞增,同時佔用空間少,生成的id僅僅是19位的整形數字,正好契合mysql的bigint資料型別,簡直完美。
資料結構:
1bit-符號位
因為二進位制中最高位是符號位,1表示負數,0表示正數。
生成的id一般都是用整數,所以最高位固定為0。
41bit-時間戳
用來記錄時間戳,毫秒級。
41位可以表示2^(41)-1個數字,
如果只用來表示正整數(計算機中正數包含0) ,可以表示的數值範圍是: 0至2^(41)-1,
減1是因為可表示的數值範圍是從0開始算的,而不是1。也就是說41位可以表示2^(41)-1個毫秒的值,轉化成單位年則是(2"41)-1)/(1000 "60 "60 -24 "365) =69年。
10bit-工作機器id
用來記錄工作機器id.可以部署在2^(10)= 1024個節點,它包括5位datacenterld和5位。
workerld
5bit可以表示的最大正整數是245-1=31,即可以用0,1,2,3…31這32個數字,來表示不同的datecenterld或workerld。
12bit-序列號
序列號,用來記錄同毫秒內產生的不同id。
12bit可以表示的最大正整數是2^(12)-1=4095,即可以用0、1.2.3. 4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號。
SnowFlake可以保證:所有生成的id按時間趨勢遞增,整個分散式系統內不會產重複id (因為有datacenterld和workerld來做區分)
優點:
(1)高效能高可用:生成時不依賴於資料庫,完全在記憶體中生成。
(2)容量大:每秒中能生成數百萬的自增ID。
(3)ID自增:存入資料庫中,索引效率高。
缺點:
依賴與系統時間的一致性,如果系統時間被回撥,或者改變,可能會造成id衝突或者重複。
總結
現在,你應該知道為什麼掘金生成的文章ID是19位數字了吧,尤其是正好契合mysql的bigint型別,非常友好。
如果你想知道在go語言裡面如何實現雪花演算法,請看我的下一篇文章:雪花演算法生成主鍵ID |青訓營筆記