分佈式ID
生成的ID使用場景
幾乎所有的業務系統,都有生成一個記錄標識的需求,例如:message_id, order_id。這個記錄標識往往就是數據庫中的唯一主鍵,數據庫上會建立聚集索引(cluster index),即在物理存儲上以這個字段排序。
在數據量大時往往需要分庫分表,這些ID經常作為取模分庫分表的依據,為了分庫分表後數據均勻,ID生成往往有“取模隨機性”的需求,所以我們通常把每秒內的序列號放在ID的最末位,保證生成的ID是隨機的。
需求分析
- 全局唯一,絕對不會出現重複的ID,且ID整體趨勢遞增。
- 高可用,服務完全基於分佈式架構。
- 高併發低延時,在4C8G的Linux上遠程調用QPS5W+,TP99在1ms內。
- 接入簡單,直接通過RPC服務或者HTTP調用即可接入。
方法一:使用數據庫的 auto_increment 來生成全局唯一遞增ID
優點:
- 簡單,使用數據庫已有的功能
- 能夠保證唯一性
- 能夠保證遞增性
- 步長固定
缺點:
- 可用性難以保證:數據庫常見架構是一主多從+讀寫分離,生成自增ID是寫請求,主庫掛了就玩不轉了
- 擴展性差,性能有上限:因為寫入是單點,數據庫主庫的寫性能決定ID的生成性能上限,並且難以擴展
改進方法:
- 增加主庫,避免寫入單點
- 數據水平切分,保證各主庫生成的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重複。