全域性唯一遞增的id 細聊分布式ID生成方法

2021-10-13 19:52:29 字數 4522 閱讀 2911

一、需求緣起

幾乎所有的業務系統,都有生成乙個記錄標識的需求,例如:

(1)訊息標識:message-id

(2)訂單標識:order-id

(3)帖子標識:tiezi-id

這個記錄標識往往就是資料庫中的唯一主鍵,資料庫上會建立聚集索引(cluster index),即在物理儲存上以這個字段排序。

這個記錄標識上的查詢,往往又有分頁或者排序的業務需求,例如:

(1)拉取最新的一頁訊息:selectmessage-id/ order by time/ limit 100

(2)拉取最新的一頁訂單:selectorder-id/ order by time/ limit 100

(3)拉取最新的一頁帖子:selecttiezi-id/ order by time/ limit 100

所以往往要有乙個time欄位,並且在time欄位上建立普通索引(non-cluster index)。

我們都知道普通索引儲存的是實際記錄的指標,其訪問效率會比聚集索引慢,如果記錄標識在生成時能夠基本按照時間有序,則可以省去這個time欄位的索引查詢:

select message-id/ (order by message-id)/limit 100

再次強調,能這麼做的前提是,message-id的生成基本是趨勢時間遞增的

這就引出了記錄標識生成(也就是上文提到的三個***-id)的兩大核心需求:

(1)全域性唯一

(2)趨勢有序

二、常見方法、不足與優化

【常見方法一:使用資料庫的 auto_increment 來生成全域性唯一遞增id】

優點:

(1)簡單,使用資料庫已有的功能

(2)能夠保證唯一性

(3)能夠保證遞增性

(4)步長固定

缺點:

(1)可用性難以保證:資料庫常見架構是一主多從+讀寫分離,生成自增id是寫請求,主庫掛了就玩不轉了

(2)擴充套件性差,效能有上限:因為寫入是單點,資料庫主庫的寫效能決定id的生成效能上限,並且難以擴充套件

改進方法:

(1)增加主庫,避免寫入單點

(2)資料水平切分,保證各主庫生成的id不重複

如上圖所述,由1個寫庫變成3個寫庫,每個寫庫設定不同的auto_increment初始值,以及相同的增長步長,以保證每個資料庫生成的id是不同的(上圖中庫0生成0,3,6,9…,庫1生成1,4,7,10,庫2生成2,5,8,11…)

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

(1)喪失了id生成的「絕對遞增性」:先訪問庫0生成0,3,再訪問庫1生成1,可能導致在非常短的時間內,id生成不是絕對遞增的(這個問題不大,我們的目標是趨勢遞增,不是絕對遞增)

(2)資料庫的寫壓力依然很大,每次生成id都要訪問資料庫

為了解決上述兩個問題,引出了第二個常見的方案

【常見方法二:單點批量id生成服務】

分布式系統之所以難,很重要的原因之一是「沒有乙個全域性時鐘,難以保證絕對的時序」,要想保證絕對的時序,還是只能使用單點服務,用本地時鐘保證「絕對時序」。資料庫寫壓力大,是因為每次生成id都訪問了資料庫,可以使用批量的方式降低資料庫寫壓力。

如上圖所述,資料庫使用雙master保證可用性,資料庫中只儲存當前id的最大值,例如0。id生成服務假設每次批量拉取6個id,服務訪問資料庫,將當前id的最大值修改為5,這樣應用訪問id生成服務索要id,id生成服務不需要每次訪問資料庫,就能依次派發0,1,2,3,4,5這些id了,當id發完後,再將id的最大值修改為11,就能再次派發6,7,8,9,10,11這些id了,於是資料庫的壓力就降低到原來的1/6了。

優點

(1)保證了id生成的絕對遞增有序

(2)大大的降低了資料庫的壓力,id生成可以做到每秒生成幾萬幾十萬個

缺點

(1)服務仍然是單點

(2)如果服務掛了,服務重啟起來之後,繼續生成id可能會不連續,中間出現空洞(服務記憶體是儲存著0,1,2,3,4,5,資料庫中max-id是5,分配到3時,服務重啟了,下次會從6開始分配,4和5就成了空洞,不過這個問題也不大)

(3)雖然每秒可以生成幾萬幾十萬個id,但畢竟還是有效能上限,無法進行水平擴充套件

改進方法

單點服務的常用高可用優化方案是「備用服務」,也叫「影子服務」,所以我們能用以下方法優化上述缺點(1):

如上圖,對外提供的服務是主服務,有乙個影子服務時刻處於備用狀態,當主服務掛了的時候影子服務頂上。這個切換的過程對呼叫方是透明的,可以自動完成,常用的技術是vip+keepalived,具體就不在這裡展開。

【常見方法三:uuid】

uuid是一種常見的方案:string id =genuuid();

優點

(1)本地生成id,不需要進行遠端呼叫,時延低

(2)擴充套件性好,基本可以認為沒有效能上限

缺點

(1)無法保證趨勢遞增

(2)uuid過長,往往用字串表示,作為主鍵建立索引查詢效率低,常見優化方案為「轉化為兩個uint64整數儲存」或者「折半儲存」(折半後不能保證唯一性)

【常見方法四:取當前毫秒數】

uuid是乙個本地演算法,生成效能高,但無法保證趨勢遞增,且作為字串id檢索效率低,有沒有一種能保證遞增的本地演算法呢?

取當前毫秒數是一種常見方案:uint64 id = gentimems();

優點

(1)本地生成id,不需要進行遠端呼叫,時延低

(2)生成的id趨勢遞增

(3)生成的id是整數,建立索引後查詢效率高

缺點

(1)如果併發量超過1000,會生成重複的id

我去,這個缺點要了命了,不能保證id的唯一性。當然,使用微秒可以降低衝突概率,但每秒最多只能生成1000000個id,再多的話就一定會衝突了,所以使用微秒並不從根本上解決問題。

【常見方法五:類snowflake演算法】

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

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

舉例,假設某公司id生成器服務的需求如下:

(1)單機高峰併發量小於1w,預計未來5年單機高峰併發量小於10w

(2)有2個機房,預計未來5年機房數量小於4個

(3)每個機房機器數小於100臺

(4)目前有5個業務線有id生成需求,預計未來業務線數量小於10個

(5)…

分析過程如下:

(1)高位取從2023年1月1日到現在的毫秒數(假設系統id生成器服務在這個時間之後上線),假設系統至少執行10年,那至少需要10年*365天*24小時*3600秒*1000毫秒=320*10^9,差不多預留39bit給毫秒數

(2)每秒的單機高峰併發量小於10w,即平均每毫秒的單機高峰併發量小於100,差不多預留7bit給每毫秒內序列號

(3)5年內機房數小於4個,預留2bit給機房標識

(4)每個機房小於100臺機器,預留7bit給每個機房內的伺服器標識

(5)業務線小於10個,預留4bit給業務線標識

這樣設計的64bit標識,可以保證:

(1)每個業務線、每個機房、每個機器生成的id都是不同的

(2)同乙個機器,每個毫秒內生成的id都是不同的

(3)同乙個機器,同乙個毫秒內,以序列號區區分保證生成的id是不同的

(4)將毫秒數放在最高位,保證生成的id是趨勢遞增的

缺點

(1)由於「沒有乙個全域性時鐘」,每台伺服器分配的id是絕對遞增的,但從全域性看,生成的id只是趨勢遞增的(有些伺服器的時間早,有些伺服器的時間晚)

最後乙個容易忽略的問題

生成的id,例如message-id/ order-id/ tiezi-id,在資料量大時往往需要分庫分表,這些id經常作為取模分庫分表的依據,為了分庫分表後資料均勻,id生成往往有「取模隨機性」的需求,所以我們通常把每秒內的序列號放在id的最末位,保證生成的id是隨機的。

又如果,我們在跨毫秒時,序列號總是歸0,會使得序列號為0的id比較多,導致生成的id取模後不均勻。解決方法是,序列號不是每次都歸0,而是歸乙個0到9的隨機數,這個地方。

分布式系統全域性唯一ID

全域性的唯一流水id 可以將乙個請求在分布式系統中的流轉路徑聚合。生成唯一id有兩種方法 持久型 使用資料庫表自增欄位或者sequence 生成,為了提高效率,每個應用節點可以快取乙個批次的id 如果機器重啟則可能會損失一部分id 但是這並不會產生任何問題。時間型 一般由機器號 業務號 時間 單節點...

分布式全域性唯一ID(二)

redis 實現分布式唯一id,其實這個也很簡單,主要使用redis string資料結構的 increment 方法。原理 使用increment方法,每次自加1,主要使用redis的高效能和單執行緒。實現方式 核心 如下,若是為了保證長度一致,其實可以預先初始化值。現在的這個是從1,2.逐漸遞增...

細聊分布式ID生成方法

幾乎所有的業務系統,都有生成乙個記錄標識的需求,例如 訊息標識 message id 訂單標識 order id 帖子標識 tiezi id 這個記錄標識往往就是資料庫中的唯一主鍵,資料庫上會建立聚集索引 cluster index 即在物理儲存上以這個字段排序。這個記錄標識上的查詢,往往又有分頁或...