淺出分佈式唯一ID生成器

0. 背景

近日由於訂單量+機器數量增加,導致原來使用時間戳生成唯一訂單號的方法行不通了,出現很多主鍵衝突
並且由於主鍵衝突導致的事務死鎖的機率也隨之加大,進而需要補的數據越來越多!因此急需一個全局唯一ID生成方式。
但由於現在單體框架還在拆解過程中,新的生成方式需要兼容單體應用(多臺集羣)
最好能用最簡單的方式先解決,待以後服務拆解出來後考慮可用性擴展性。

1. 調研

大概有以下幾個選擇,優先級由高到低:

  1. snowflake
  2. 數據庫唯一ID主鍵約束
  3. redis 原子遞增
  4. UUID
  5. leaf
  6. uid-generator

優先級 snowflake 主要是有以下理由:

  1. snowflake 現階段用一個工具類就能直接上手使用
  2. 現在分庫分表也使用了 snowflake,以後可以做統一的唯一ID服務
  3. 很多分佈式ID中間件都基於 snowflake,以後也方便擴展,而不會出現由於算法不同,新算法生成的序列號可能被以前已經生成過

2. 實際使用

  1. 開始考慮使用IP的最後一位作爲 workid,因爲已經和運維確認過,現在線上機器是最後是連號,不會重複,但是因爲默認 workid 最大是 32,就沒有將 datacenterid 都改爲 workid,因爲我發現我們 hostname 是帶編號的,而且是遞增的規律:xxservice001,最後三位是序號,因此直接取最後三位數字,作爲 workid。

snowflake 默認使用 1+41+10+12 ID組裝模式,其中 10 bit 是 datacenter + workid

2.1 時鐘回撥問題

關於時鐘回撥,一般解決方法有:

  1. 機器之間定時同步時間
  2. 通過請求其它機器獲取平均時間戳對比(Leaf)
  3. 在閾值內不斷獲取當前時間直到大於上次申請時間
  4. 使用第三方存儲號段來解決(我個人傾向號段方案)

由於正面臨拆分,以後會有專門的 id 生成服務,因此現在並沒有在單體服務上做時鐘回撥的解決,我選擇最簡單的 hutool 工具引用即可,但是它仍可能有一定機率有時鐘回撥。

現階段運行了兩週,還沒發現這個錯誤。

3. 其它ID算法簡析

稍微系統的學習一下業內的分佈式ID生成方案

3.1 uid-generator

以中間件的形式提供服務,基於 snowflake 的優化版,使用雙 buffer 環(我個人理解爲生產者和消費者模式),一個 buffer 生產號段,一個 buffer 消費號段。

snowflake 各個分配可自定義:

  • 1bit:第一位不用,用於標誌 0 ,表示正數
  • 28bits:每個機器當前時間,單位秒
  • 22bits:機器數量
  • 13bits:每秒的併發數
  1. 默認是秒級併發,常規的 snowflake 算法是基於毫秒級(41 bits),但是沒有必要這麼精確,每臺機器每秒併發最多8192夠用了。
  2. 接着是環形數組,通俗來講就是生產者和消費者模型,一個不能生產過快,一個不能消費過快。同時該模型又有擴容機制,即生產者到一定量後擴容。生產者每次生產者的數量。由於是週期性填充生產並緩存,因此可以接受短時間內第三方存儲服務不可用。
  3. 默認支持通過 mysql 存儲機器申請情況,這可以用其它來代替,例如 zk,但是這需要我依賴的需要高可用

由此可見, 不管如何配置, CachedUidGenerator總能提供600萬/s的穩定吞吐量, 只是使用年限會有所減少. 這真的是太棒了
https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

3.2 leaf

也是以中間件的形式提供服務。

目前Leaf的性能在4C8G的機器上QPS能壓測到近5w/s,TP999 1ms,已經能夠滿足大部分的業務的需求

有兩種生產號段方式:Leaf-segmentLeaf-snowflake

3.2.1 Leaf-segment

基於 mysql 數據庫的號段模式,不是每一次都訪問數據庫,而是在數據庫記錄哪臺機器分配了哪個號段區間。

在每次消費時,當前機器訪問 Leaf 時,Leaf 會得到該機器是否已經發了號段,並判斷下一個號段的狀態,接着從數據庫中更新號段信息,並將號段到載入內存中,防止出現當前號段消費完後,加載下一個號段時的卡頓。

由此得知可以短暫的容忍 mysql 一段時間內不可用。

3.2.2 Leaf-snowflake

標準的 1+41+10+12 ID組裝模式,默認使用 zk 做 leaf 集羣多機房部署以達到高可用,主要是利用臨時節點,記錄當前正在訪問 leaf 的消費端。

解決了時鐘回撥問題:3s一次上傳 leaf 機器節點當前的時間,並進行 RPC 調用其它機器的時間,得到平均時間後,有個閾值(考慮網絡延遲)進而通過判斷當前機器時間與平均時間,來解決時鐘回撥的問題。

3.3 UUID

唯一,但是無序,且長度太長,一般來說,數據庫存都是 bigint(20) 即 64 bit,對應在 java 中使用 long,也是 64bit,而 uuid 是 128 bit,且是字母數字組合,需要 char(128),空間浪費很嚴重

3.4 redis

基於 Redis 全局遞增,步長設置。
使用 lua 腳本結合 snowflake 來實現

例如有三臺業務機器,兩臺 redis 作爲id生成機器:1+41+10+12 模式,其中的 workid 使用 redis 集羣中的機器編號,這樣就會請求的每個 redis 獲取的 redis 都不會重複,但是仍舊有時鐘回撥的問題。

3.5 idx_mysql_id

基於 mysql 的主鍵,強依賴mysql,而且性能低,適合前期使用,併發量大時,不僅每次要訪問數據庫,而且還要做保證數據庫的高可用

3.6 snowflake

基本上現在主流的都會或多或少參考該算法
一共64bit,

  1. 1bit默認爲0,表示正數
  2. 41bit作爲時間戳毫秒值
  3. 10bit作爲工作機器
  4. 12bit作爲每個機器每毫秒下最多能支持多少個併發請求生成號

cn.hutool.core.lang.Snowflake

// 獲取當前時間,hutool 內部有個定時任務刷時間
long timestamp = genTime();
// 將當前時間與上次申請時間對比,判斷是否出現時鐘回撥
if (timestamp < lastTimestamp) {
     //如果服務器時間有問題(時鐘回撥) 報錯。
    throw new IllegalStateException(StrUtil.format("Clock moved backwards. Refusing to generate id for {}ms", lastTimestamp - timestamp));
}
// 微秒下的併發走這裏
if (lastTimestamp == timestamp) {
    // 微秒下進行序號 +1,默認最多支持微秒 8192 併發量
    sequence = (sequence + 1) & sequenceMask;
    if (sequence == 0) {
        timestamp = tilNextMillis(lastTimestamp);
    }
} else {
    // 併發數重置
    sequence = 0L;
}
// 重置上一次時間
lastTimestamp = timestamp;

4. 個人認爲好的解決方案

4.1 號段

記錄請求的機器ip+port(能唯一定位該機器的標識),然後分配號段,例如1000~3000,另一個機器過來請求,分配另一個號段,例如 4000~9000,這樣。主要是依賴第三方存儲號段分配情況,例如使用數據庫,Redis,Zookeeper 都可以。沒有

4.2 算法生成

就是 snowflake,不同在於根據業務的不同,可以對其中的 bit 做不同的分配,例如毫秒位改爲秒位,這樣多了三個位可以放給機器位,適合併發量不大,但是機器多的場景,或者機器位給最後的併發請求量位,適合併發量大,機器數量比 1024 少的場景。

而且這個也可以進化爲號段,但是會有很多永遠都不會用到的號段,需要有個重複利用的方案。但是這解決了普通號段模式的訂單有序性問題。

5. 參考

分佈是唯一ID系列5篇:https://www.cnblogs.com/itqiankun/p/11350857.html
美團分佈式ID系列2篇:https://tech.meituan.com/2019/03/07/open-source-project-leaf.html
多key的預備:https://mp.weixin.qq.com/s/PCzRAZa9n4aJwHOX-kAhtA?

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章