分佈式ID

生成的ID使用場景

幾乎所有的業務系統,都有生成一個記錄標識的需求,例如:message_id, order_id。這個記錄標識往往就是數據庫中的唯一主鍵,數據庫上會建立聚集索引(cluster index),即在物理存儲上以這個字段排序。

在數據量大時往往需要分庫分表,這些ID經常作為取模分庫分表的依據,為了分庫分表後數據均勻,ID生成往往有“取模隨機性”的需求,所以我們通常把每秒內的序列號放在ID的最末位,保證生成的ID是隨機的。

需求分析

  1. 全局唯一,絕對不會出現重複的ID,且ID整體趨勢遞增。
  2. 高可用,服務完全基於分佈式架構。
  3. 高併發低延時,在4C8G的Linux上遠程調用QPS5W+,TP99在1ms內。
  4. 接入簡單,直接通過RPC服務或者HTTP調用即可接入。

方法一:使用數據庫的 auto_increment 來生成全局唯一遞增ID

優點:

  • 簡單,使用數據庫已有的功能
  • 能夠保證唯一性
  • 能夠保證遞增性
  • 步長固定

缺點:

  • 可用性難以保證:數據庫常見架構是一主多從+讀寫分離,生成自增ID是寫請求,主庫掛了就玩不轉了
  • 擴展性差,性能有上限:因為寫入是單點,數據庫主庫的寫性能決定ID的生成性能上限,並且難以擴展

改進方法:

  1. 增加主庫,避免寫入單點
  2. 數據水平切分,保證各主庫生成的ID不重複

每個寫庫設置不同的auto_increment初始值,以及相同的增長步長,以保證每個數據庫生成的ID是不同的(上圖中庫0生成0,3,6,9…,庫1生成1,4,7,10,庫2生成2,5,8,11…)

改進後的架構保證了可用性,但缺點是:

  • 喪失了ID生成的“絕對遞增性”:先訪問庫0生成0,3,再訪問庫1生成1,可能導致在非常短的時間內,ID生成不是絕對遞增的(這個問題不大,我們的目標是趨勢遞增,不是絕對遞增)
  • 數據庫的寫壓力依然很大,每次生成ID都要訪問數據庫

方法二:uuid

方案一不管是數據庫還是服務來生成ID,業務方Application都需要進行一次遠程調用,比較耗時。有沒有一種本地生成ID的方法,即高性能,又時延低呢?

uuid是一種常見的方案:string ID =GenUUID();

優點

  • 本地生成ID,不需要進行遠程調用,時延低
  • 擴展性好,基本可以認為沒有性能上限

缺點

  • 無法保證趨勢遞增
  • uuid過長,往往用字符串表示,作為主鍵建立索引查詢效率低,常見優化方案為“轉化為兩個uint64整數存儲”或者“折半存儲”(折半後不能保證唯一性)

方法三:時間戳

優點:

  • 本地生成ID,無需遠程調用,低延時
  • ID趨勢遞增
  • ID是整數,建立索引後查詢效率高

缺點:

  • 如果併發量超過1000,會生成重複的ID

方法四:隨機數+數據庫唯一索引

優點:

  • 本地生成ID,無需遠程調用,低延時
  • ID趨勢遞增
  • ID是整數,建立索引後查詢效率高

缺點:

  • 依賴數據庫
  • 性能一般,直到持久化的時候才能確定id的唯一性

方法五:類snowflake算法

snowflake是twitter開源的分佈式ID生成算法,其核心思想是:一個long型的ID,使用其中41bit作為毫秒數,10bit作為機器編號,12bit作為毫秒內序列號。這個算法單機每秒內理論上最多可以生成1000*(2^12),也就是400W的ID,完全能滿足業務的需求。

借鑑snowflake的思想,結合各公司的業務邏輯和併發量,可以實現自己的分佈式ID生成算法。

優點:

  • 快(哈哈,天下武功唯快不破)。
  • 沒有啥依賴,實現也特別簡單。
  • 知道原理之後可以根據實際情況調整各各位段,方便靈活。

缺點:

  • 只能趨勢遞增。(如果絕對遞增,競爭對手中午下單,第二天在下單即可大概判斷該公司的訂單量)
  • 依賴機器時間,如果發生回撥會導致可能生成id重複。