掘金的文章ID是怎麼生成的?

語言: CN / TW / HK

本文已參與「新人創作禮」活動,一起開啟掘金創作之路

引言

在掘金寫了多篇文章之後,我發現了掘金生成文章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資料型別,簡直完美。

資料結構:

image.png

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 |青訓營筆記