10.14 一線大廠的分佈式唯一ID生成方案

一、前言

分佈式系統中我們會對一些數據量大的業務進行分拆,如:用戶表,訂單表。因為數據量巨大一張表無法承接,就會對其進行分庫分表。小夥伴們可以去看一下《分庫分表?如何做到永不遷移數據和避免熱點?》

但一旦涉及到分庫分表,就會引申出分佈式系統中唯一主鍵ID的生成問題,永不遷移數據和避免熱點的文章中要求需要唯一ID的特性:

  • 整個系統ID唯一
  • ID是數字類型,而且是趨勢遞增的
  • ID簡短,查詢效率快

什麼是遞增?如:第一次生成的ID為12,下一次生成的ID是13,再下一次生成的ID是14。這個就是生成ID遞增。

什麼是趨勢遞增?如:在一段時間內,生成的ID是遞增的趨勢。如:再一段時間內生成的ID在【0,1000】之間,過段時間生成的ID在【1000,2000】之間。但在【0-1000】區間內的時候,ID生成有可能第一次是12,第二次是10,第三次是14。

那有什麼方案呢?往下看!

一線大廠的分佈式唯一ID生成方案

二、分佈式ID的幾種生成方案

2.1、UUID

這個方案是小夥伴們第一個能過考慮到的方案

優點:

  • 代碼實現簡單。
  • 本機生成,沒有性能問題
  • 因為是全球唯一的ID,所以遷移數據容易

缺點:

  • 每次生成的ID是無序的,無法保證趨勢遞增
  • UUID的字符串存儲,查詢效率慢
  • 存儲空間大
  • ID本事無業務含義,不可讀

應用場景:

  • 類似生成token令牌的場景
  • 不適用一些要求有趨勢遞增的ID場景

此UUID方案是不適用老顧的需求。

2.2、MySQL主鍵自增

這個方案就是利用了MySQL的主鍵自增auto_increment,默認每次ID加1。

優點:

  • 數字化,id遞增
  • 查詢效率高
  • 具有一定的業務可讀

缺點:

  • 存在單點問題,如果mysql掛了,就沒法生成iD了
  • 數據庫壓力大,高併發抗不住

2.3、MySQL多實例主鍵自增

這個方案就是解決mysql的單點問題,在auto_increment基本上面,設置step步長

一線大廠的分佈式唯一ID生成方案


每臺的初始值分別為1,2,3...N,步長為N(這個案例步長為4)

優點:

  • 解決了單點問題

缺點:

  • 一旦把步長定好後,就無法擴容;而且單個數據庫的壓力大,數據庫自身性能無法滿足高併發

應用場景:

  • 數據不需要擴容的場景

此方案也不滿足老顧的需求,因為不方便擴容(記住這個方案,嘿嘿)

2.4、雪花snowflake算法

這個算法網上介紹了很多,老顧這裡就不詳細介紹。雪花算法生成64位的二進制正整數,然後轉換成10進制的數。64位二進制數由如下部分組成:

一線大廠的分佈式唯一ID生成方案


  • 1位標識符:始終是0
  • 41位時間戳:41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截 )得到的值,這裡的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的
  • 10位機器標識碼:可以部署在1024個節點,如果機器分機房(IDC)部署,這10位可以由 5位機房ID + 5位機器ID 組成
  • 12位序列:毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號

優點:

  • 此方案每秒能夠產生409.6萬個ID,性能快
  • 時間戳在高位,自增序列在低位,整個ID是趨勢遞增的,按照時間有序遞增
  • 靈活度高,可以根據業務需求,調整bit位的劃分,滿足不同的需求

缺點:

  • 依賴機器的時鐘,如果服務器時鐘回撥,會導致重複ID生成

在分佈式場景中,服務器時鐘回撥會經常遇到,一般存在10ms之間的回撥;小夥伴們就說這點10ms,很短可以不考慮吧。但此算法就是建立在毫秒級別的生成方案,一旦回撥,就很有可能存在重複ID。

此方案暫不符合老顧的需求(嘿嘿,看看怎麼優化這個方案,小夥伴們先記住)

2.5、Redis生成方案

利用redis的incr原子性操作自增,一般算法為:

年份 + 當天距當年第多少天 + 天數 + 小時 + redis自增

優點:

  • 有序遞增,可讀性強

缺點:

  • 佔用帶寬,每次要向redis進行請求

整體測試了這個性能如下:

需求:同時10萬個請求獲取ID
1、併發執行完耗時:9s左右
2、單任務平均耗時:74ms
3、單線程最小耗時:不到1ms
4、單線程最大耗時:4.1s

性能還可以,如果對性能要求不是太高的話,這個方案基本符合老顧的要求。

但不完全符合業務老顧希望id從 1 開始趨勢遞增。(當然算法可以調整為 就一個 redis自增,不需要什麼年份,多少天等)。

2.6、小結

以上介紹了常見的幾種分佈式ID生成方案。一線大廠的分佈式ID方案絕沒有這個簡單,他們對高併發,高可用的要求很高。

如Redis方案中,每次都要去Redis去請求,有網絡請求耗時,併發強依賴了Redis。這個設計是有風險的,一旦Redis掛了,整個系統不可用。

而且一線大廠也會考慮到ID安全性的問題,如:Redis方案中,用戶是可以預測下一個ID號是多少,因為算法是遞增的。

這樣的話競爭對手第一天中午12點下個訂單,就可以看到平臺的訂單ID是多少,第二天中午12點再下一單,又平臺訂單ID到多少。這樣就可以猜到平臺1天能產生多少訂單了,這個是絕對不允許的,公司絕密啊。

三、一線大廠是如何設計的呢?

一線大廠的設計思路其實和小夥伴們思路差不多,只是多想了1~2層,設計上面多了1~2個環節。

3.1、改造數據庫主鍵自增

上述我們介紹了利用數據庫的自增主鍵的特性,可以實現分佈式ID;這個ID比較簡短明瞭,適合做userId,正好符合如何永不遷移數據和避免熱點? 根據服務器指標分配數據量(揭秘篇)文章中的ID的需求。但這個方案有嚴重的問題:

  • 一旦步長定下來,不容易擴容
  • 數據庫壓力山大

小夥伴們看看怎麼優化這個方案。先看數據庫壓力大,為什麼壓力大?是因為我們每次獲取ID的時候,都要去數據庫請求一次。那我們可以不可以不要每次去取?

思路我們可以請求數據庫得到ID的時候,可設計成獲得的ID是一個ID區間段。

一線大廠的分佈式唯一ID生成方案


我們看上圖,有張ID規則表:

1、id表示為主鍵,無業務含義。2、biz_tag為了表示業務,因為整體系統中會有很多業務需要生成ID,這樣可以共用一張表維護3、max_id表示現在整體系統中已經分配的最大ID4、desc描述5、update_time表示每次取的ID時間

我們再來看看整體流程:

1、【用戶服務】在註冊一個用戶時,需要一個用戶ID;會請求【生成ID服務(是獨立的應用)】的接口2、【生成ID服務】會去查詢數據庫,找到user_tag的id,現在的max_id為0,step=10003、【生成ID服務】把max_id和step返回給【用戶服務】;並且把max_id更新為max_id = max_id + step,即更新為10004、【用戶服務】獲得max_id=0,step=1000;5、 這個用戶服務可以用ID=【max_id + 1,max_id+step】區間的ID,即為【1,1000】6、【用戶服務】會把這個區間保存到jvm中7、【用戶服務】需要用到ID的時候,在區間【1,1000】中依次獲取id,可採用AtomicLong中的getAndIncrement方法。8、如果把區間的值用完了,再去請求【生產ID服務】接口,獲取到max_id為1000,即可以用【max_id + 1,max_id+step】區間的ID,即為【1001,2000】

這個方案就非常完美的解決了數據庫自增的問題,而且可以自行定義max_id的起點,和step步長,非常方便擴容。

而且也解決了數據庫壓力的問題,因為在一段區間內,是在jvm內存中獲取的,而不需要每次請求數據庫。即使數據庫宕機了,系統也不受影響,ID還能維持一段時間。

3.2、競爭問題

以上方案中,如果是多個用戶服務,同時獲取ID,同時去請求【ID服務】,在獲取max_id的時候會存在併發問題。

如用戶服務A,取到的max_id=1000 ;用戶服務B取到的也是max_id=1000,那就出現了問題,Id重複了。那怎麼解決?

其實方案很多,加分佈式鎖,保證同一時刻只有一個用戶服務獲取max_id。當然也可以用數據庫自身的鎖去解決。

利用事務方式加行鎖,上面的語句,在沒有執行完之前,是不允許第二個用戶服務請求過來的,第二個請求只能阻塞。

3.3、突發阻塞問題

一線大廠的分佈式唯一ID生成方案


上圖中,多個用戶服務獲取到了各自的ID區間,在高併發場景下,ID用的很快,如果3個用戶服務在某一時刻都用完了,同時去請求【ID服務】。因為上面提到的競爭問題,所有隻有一個用戶服務去操作數據庫,其他二個會被阻塞。

小夥伴就會問,有這麼巧嗎?同時ID用完。我們這裡舉的是3個用戶服務,感覺概率不大;如果是100個用戶服務呢?概率是不是一下子大了。

出現的現象就是一會兒突然系統耗時變長,一會兒好了,就是這個原因導致的,怎麼去解決?

3.4、雙buffer方案

在一般的系統設計中,雙buffer會經常看到,怎麼去解決上面的問題也可以採用雙buffer方案。

一線大廠的分佈式唯一ID生成方案


在設計的時候,採用雙buffer方案,上圖的流程:

1、當前獲取ID在buffer1中,每次獲取ID在buffer1中獲取2、當buffer1中的Id已經使用到了100,也就是達到區間的10%3、達到了10%,先判斷buffer2中有沒有去獲取過,如果沒有就立即發起請求獲取ID線程,此線程把獲取到的ID,設置到buffer2中。4、如果buffer1用完了,會自動切換到buffer25、buffer2用到10%了,也會啟動線程再次獲取,設置到buffer1中6、依次往返

雙buffer的方案,小夥伴們有沒有感覺很酷,這樣就達到了業務場景用的ID,都是在jvm內存中獲得的,從此不需要到數據庫中獲取了。允許數據庫宕機時間更長了。

因為會有一個線程,會觀察什麼時候去自動獲取。兩個buffer之間自行切換使用。就解決了突發阻塞的問題。

四、總結

此方案是某團使用的分佈式ID算法,小夥伴們如果想了解更深,可以去網上搜下,這裡應該介紹了比較詳細了。

當然此方案美團還做了一些別的優化,監控ID使用頻率,自動設置步長step,從而達到對ID節省使用。

此ID方案非常適合《分庫分表?如何做到永不遷移數據和避免熱點?》中的ID需求。


分享到:


相關文章: