今天咱們來拆解 Snowflake 算法,同時領略百度、美團、騰訊等大廠在全局惟一 ID 服務方面作的設計,接着根據具體需求設計一款全新的全局惟一 ID 生成算法。這還不夠,咱們會討論到全局惟一 ID 服務的分佈式 CAP 選擇與性能瓶頸。html
已經熟悉 Snowflake 的朋友能夠先去看大廠的設計和權衡。git
百度 UIDGenertor:github.com/baidu/uid-g…github
美團 Leaf:tech.meituan.com/2017/04/21/…算法
騰訊 Seqsvr: www.infoq.cn/article/wec…數據庫
全局惟一 ID 是分佈式系統和訂單類業務系統中重要的基礎設施。這裏引用美團的描述:編程
在複雜分佈式系統中,每每須要對大量的數據和消息進行惟一標識。如在美團點評的金融、支付、餐飲、酒店、貓眼電影等產品的系統中,數據日漸增加,對數據分庫分表後須要有一個惟一 ID 來標識一條數據或消息,數據庫的自增 ID 顯然不能知足需求;特別一點的如訂單、騎手、優惠券也都須要有惟一 ID 作標識。數組
這時候你可能會問:我仍是不懂,爲何必定要全局惟一 ID?緩存
我再列舉一個場景,在 MySQL 分庫分表的條件下,MySQL 沒法作到依次、順序、交替地生成 ID,這時候要保證數據的順序,全局惟一 ID 就是一個很好的選擇。安全
在爬蟲場景中,這條數據在進入數據庫以前會進行數據清洗、校驗、矯正、分析等多個流程,這期間有必定機率發生重試或設爲異常等操做,也就是說在進入數據庫以前它就須要有一個 ID 來標識它。性能優化
美團技術團隊列出的 4 點屬性我以爲很準確,它們是:
看上去第 3 點和第 4 點彷佛還存在些許衝突,這個後面再說。除了以上列舉的 ID 屬性外,基於這個生成算法構建的服務還須要買足高 QPS、高可用性和低延遲的幾個要求。
你們在念書的時候確定都學過 UUID 和 GUID,它們生成的值看上去像這樣:
6F9619FF-8B86-D011-B42D-00C04FC964FF
複製代碼
因爲不是純數字組成,這就沒法知足趨勢遞增和單調遞增這兩個屬性,同時在寫入時也會下降寫入性能。上面提到了數據庫自增 ID 沒法知足入庫前使用和分佈式場景下的需求,遂排除。
有人提出了藉助 Redis 來實現,例如訂單號=日期+當日自增加號,自增加經過 INCR 實現。但這樣操做的話又沒法知足編號不可猜想需求。
這時候有人提出了 MongoDB 的 ObjectID,不要忘了它生成的 ID 是這樣的: 5b6b3171599d6215a8007se0
,和 UUID 同樣沒法知足遞增屬性,且和 MySQL 同樣要入庫後才能生成。
難道就沒有能打的了嗎?
Twitter 於 2010 年開源了內部團隊在用的一款全局惟一 ID 生成算法 Snowflake,翻譯過來叫作雪花算法。Snowflake 不借助數據庫,可直接由編程語言生成,它經過巧妙的位設計使得 ID 可以知足遞增屬性,且生成的 ID 並非依次連續的,可以知足上面提到的全局惟一 ID 的 4 個屬性。它連續生成的 3 個 ID 看起來像這樣:
563583455628754944
563583466173235200
563583552944996352
複製代碼
Snowflake 以 64 bit 來存儲組成 ID 的4 個部分:
一、最高位佔1 bit,值固定爲 0,以保證生成的 ID 爲正數;
二、中位佔 41 bit,值爲毫秒級時間戳;
三、中下位佔 10 bit,值爲工做機器的 ID,值的上限爲 1024;
四、末位佔 12 bit,值爲當前毫秒內生成的不一樣 ID,值的上限爲 4096;
Snowflake 的代碼實現網上有不少款,基本上各大語言都能找到實現參考。我以前在作實驗的時候在網上找到一份 Golang 的代碼實現:
代碼可在個人 Gist 查看和下載。
snowflake 不依賴數據庫,也不依賴內存存儲,隨時可生成 ID,這也是它如此受歡迎的緣由。但由於它在設計時經過時間戳來避免對內存和數據庫的依賴,因此它依賴於服務器的時間。上面咱們提到了 Snowflake 的 4 段結構,實際上影響 ID 大小的是較高位的值,因爲最高位固定爲 0,遂影響 ID 大小的是中位的值,也就是時間戳。
試想,服務器的時間發生了錯亂或者回撥,這就直接影響到生成的 ID,有很大機率生成重複的 ID 且必定會打破遞增屬性。這是一個致命缺點,你想一想,支付訂單和購買訂單的編號重複,這是多麼嚴重的問題!
另外,因爲它的中下位和末位 bit 數限制,它每毫秒生成 ID 的上限嚴重受到限制。因爲中位是 41 bit 的毫秒級時間戳,因此從當前起始到 41 bit 耗盡,也只能堅持 70 年。
再有,程序獲取操做系統時間會耗費較多時間,相比於隨機數和常數來講,性能相差太遠,這是制約它生成性能的最大因素。
長話短說,咱們來看看百度、美團、騰訊(微信)是如何作的。
百度團隊開源了 UIDGenerator 算法.
它經過借用將來時間和雙 Buffer 來解決時間回撥與生成性能等問題,同時結合 MySQL 進行 ID 分配。這是一種基於 Snowflake 的優化操做,是一個好的選擇,你認爲這是否是優選呢?
美團團隊根據業務場景提出了基於號段思想的 Leaf-Segment 方案和基於 Snowflake 的 Leaf-Snowflake 方案.
出現兩種方案的緣由是 Leaf-Segment 並無知足安全屬性要求,容易被猜想,沒法用在對外開放的場景(如訂單)。Leaf-Snowflake 經過文件系統緩存下降了對 ZooKeeper 的依賴,同時經過對時間的比對和警報來應對 Snowflake 的時間回撥問題。這兩種都是一個好的選擇,你認爲這是否是優選呢?
微信團隊業務特殊,它有一個用 ID 來標記消息的順序的場景,用來確保咱們收到的消息就是有序的。在這裏不是全局惟一 ID,而是單個用戶全局惟一 ID,只須要保證這個用戶發送的消息的 ID 是遞增便可。
這個項目叫作 Seqsvr,它並無依賴時間,而是經過自增數和號段來解決生成問題的。這是一個好的選擇,你認爲這是否是優選呢?
在瞭解 Snowflake 的優缺點、閱讀了百度 UIDGenertor、美團 Leaf 和騰訊微信 Seqsvr 的設計後,我但願設計出一款可以知足全局惟一 ID 4 個屬性且性能更高、使用期限更長、不受單位時間限制、不依賴時間的全局惟一 ID 生成算法。
這看起來很簡單,但吸取所學知識、設計、實踐和性能優化佔用了我 4 個週末的時間。在我看來,這個算法的設計過程就像是液態的水轉換爲氣狀的霧同樣,遂我給這個算法取名爲薄霧(Mist)算法。接下來咱們來看看薄霧算法是如何設計和實現的。
位數是影響 ID 數值上限的主要因素,Snowflake 中下位和末位的 bit 數限制了單位時間內生成 ID 的上限,要解決這個兩個問題,就必須從新設計 ID 的組成。
拋開中位,咱們先看看中下位和末位的設計。中下位的 10 bit 的值實際上是機器編號,末位 12 bit 的值實際上是單位時間(同一毫秒)內生成的 ID 序列號,表達的是這毫秒生成的第 5 個或第 150 個 數值,同時兩者的組合使得 ID 的值變幻莫測,知足了安全屬性。實際上並不須要記錄機器編號,也能夠不用管它究竟是單位時間內生成的第幾個數值,安全屬性咱們能夠經過多組隨機數組合的方式實現,隨着數字的遞增和隨機數的變幻,經過 ID 猜順序的難度是很高的。
最高位固定是 0,不須要對它進行改動。咱們來看看相當重要的中位,Snowflake 的中位是毫秒級時間戳,既然不打算依賴時間,那麼確定也不會用時間戳,用什麼呢?我選擇自增數 1,2,3,4,5,...
。中位決定了生成 ID 的上限和使用期限,若是沿用 41 bit,那麼上限跟用時間戳的上限相差無幾,通過計算後我選擇採用與 Snowflake 的不一樣的分段:
縮減中下位和末位的 bit 數,增長中位的 bit 數,這樣就能夠擁有更高的上限和使用年限,那上限和年限如今是多久呢?中位數值的上限計算公式爲 int64(1<<47 - 1)
,計算結果爲 140737488355327
。百萬億級的數值,假設天天消耗 10 億 ID,薄霧算法能用 385+ 年,幾輩子都用不完。
中下位和末位都是 8 bit,數值上限是 255,即開閉區間是 [0, 255]。這兩段若是用隨機數進行填充,對應的組合方式有 256 * 256
種,且每次都會變化,猜想難度至關高。因爲不像 Snowflake 那樣須要計算末位的序列號,遂薄霧算法的代碼並不長,具體代碼可在個人 GitHub 倉庫找到:
聊聊性能問題,獲取時間戳是比較耗費性能的,不獲取時間戳速度固然快了,那 500+ 倍是如何得來的呢?以 Golang 爲例(我用 Golang 作過實驗),Golang 隨機數有三種生成方式:
基於固定數值種子的隨機數每次生成的值都是同樣的,是僞隨機,不可用在此處。將時間戳做爲種子以生成隨機數是目前 Golang 開發者的主流作法,實測性能約爲 8800 ns/op。大數真隨機知道的人比較少,實測性能 335ns/op,因而可知性能相差近 30 倍。
大數真隨機也有必定的損耗,若是想要將性能提高到頂點,只須要將中下位和末位的隨機數換成常數便可,常數實測性能 15ns/op,是時間戳種子隨機數的 587 倍。
要注意的是,將常數放到中下位和末位的性能是很高,可是猜想難度也相應降低。
薄霧算法爲了避開時間依賴,不得不依賴存儲,中位自增的數值只能在內存中存活,遂須要依賴存儲將自增數值存儲起來,避免由於宕機或程序異常形成重複 ID 的事故。
看起來是這樣,但它真的是依賴存儲嗎?
你想一想,這麼重要的服務一定要求高可用,不管你用 Twitter 仍是百度或者美團、騰訊微信的解決方案,在架構上必定都是高可用的,高可用必定須要存儲。在這樣的背景下,薄霧算法的依賴其實並非額外的依賴,而是能夠與架構徹底融合到一塊兒的設計。
既然提出了薄霧算法,怎麼能不提供真實可用的工程實踐呢?在編寫完薄霧算法以後,我就開始了工程實踐的工做,將薄霧算法與 KV 存儲結合到一塊兒,提供全局惟一 ID 生成服務。這裏我選擇了較爲熟悉的 Redis,Mist 與 Redis 的結合,我爲這個項目取的名字爲 Medis。
性能高並非編造出來的,咱們看看它 Jemeter 壓測參數和結果:
以上是 Medis README 中給出的性能測試截圖,在大基數條件下的性能約爲 2.5w/sec。這麼高的性能除了薄霧算法自己高性能以外,Medis 的設計也做出了很大貢獻:
Medis 服務啓動流程和接口訪問流程圖下所示:
感興趣的朋友能夠下載體驗一下,啓動 Medis 根目錄的 server.go 後,訪問 http://localhost:1558/sequence 便能拿到全局惟一 ID。
分佈式 CAP (一致性、可用性、分區容錯性)已成定局,這類服務一般追求的是可用性架構(AP)。因爲設計中採用了預存預取,且要保持總體順序遞增,遂單機提供訪問是優選,即分佈式架構下的性能上限就是提供服務的那臺主機的單機性能。
你想要實現分佈式多機提供服務?
這樣的需求要改動 Medis 的邏輯,同時也須要改動各應用之間的組合關係。若是要實現分佈式多機同時提供服務,那麼就要廢棄 Redis 和 Channel 預存預取機制,接着放棄 Channel 而改用即時生成,這樣即可以同時使用多個 Server,但性能的瓶頸就轉移到了 KV 存儲(這裏是 Redis),性能等同於單機 Redis 的性能。你能夠採用 ETCD 或者 Zookeeper 來實現多 KV,但這不是又回到了 CAP 原點了嗎?
至於怎麼選擇,可根據實際業務場景和需求與架構進行討論,選擇一個適合的方案進行部署便可。
領略了 Mist 和 Medis 的風采後,相信你必定會有其餘巧妙的想法,歡迎在評論區留言,咱們一塊兒交流進步!
夜幕團隊成立於 2019 年,團隊包括崔慶才(靜覓)、周子淇(Loco)、陳祥安(CXA)、唐軼飛(大魚|BruceDone)、馮威(妄爲)、蔡晉(悅來客棧的老闆)、戴煌金(鹹魚)、張冶青(MarvinZ)、韋世東(Asyncins|奎因)和文安哲(sml2h3)。
涉獵的編程語言包括但不限於 Python、Rust、C++、Go,領域涵蓋爬蟲、深度學習、服務研發、逆向工程、軟件安全等。團隊非正亦非邪,只作認爲對的事情,請你們當心。