淺談分散式 ID 的實踐與應用

seven123發表於2021-01-26

在業務系統中很多場景下需要生成不重複的 ID,比如訂單編號、支付流水單號、優惠券編號等都需要使用到。本文將介紹分散式 ID 的產生原因,以及目前業界常用的四種分散式 ID 實現方案,並且詳細介紹其中兩種的實現以及優缺點,希望可以給您帶來 關於分散式 ID 的啟發

為什麼要用分散式 ID

隨著業務資料量的增長,儲存在資料庫中的資料越來越多,當索引佔用的空間超出可用記憶體大小後,就會透過磁碟索引來查詢資料,這樣就會極大的降低資料查詢速度。如何解決這樣的問題呢?一般我們首先透過分庫分表來解決,分庫分表後就無法使用資料庫自增 ID 來作為資料的唯一編號,那麼就需要 使用分散式 ID 來做唯一編號 了。

分散式 ID 實現方案

目前,關於分散式 ID ,業界主要有以下四種實現方案:

·  UUID :使用 JDK 的 UUID#randomUUID() 生成的 ID;

·  Redis 的原子自增 :使用 Jedis#incr(String key) 生成的 ID;

·  Snowflake 演算法 :以時間戳機器號和毫秒內併發組成的 64 位 Long 型 ID;

·  分段步長 :按照步長從資料庫讀取一段可用範圍的 ID;

我們總結一下這幾種方案的特點:

方案  

順序性  

重複性  

可用性  

部署方式  

可用時間  

UUID  

無序  

透過多位隨機字元達到極低重複機率,但理論上是會重複的  

一直可用  

JDK 直接呼叫  

永久  

Redis  

單調遞增  

RDB 持久化模式下,會出現重複  

Redis 當機後不可用  

Jedis 客戶端呼叫  

永久  

Snowflake  

趨勢遞增  

不會重複  

發生時鐘回撥並且回撥時間超過等待閾值時不可用  

整合部署、叢集部署  

69年  

分段步長  

趨勢遞增  

不會重複  

如果資料庫當機並且獲取步長內的 ID 用完後不可用

整合部署、叢集部署  

永久  

前面兩種實現方案的用法以及實現大家日常瞭解較多,就不在此贅述...本文我們會詳細介紹 Snowflake 演算法以及分段步長方案。

Snowflake 演算法可以做到分配好機器號後就可以使用,不依賴任何第三方服務實現本地 ID 生成,依賴的第三方服務越少可用性越高,那麼我們先來介紹一下 Snowflake 演算法。

Snowflake 演算法

長整型數字(即 Long 型數字)的十進位制範圍是 -2^64 到 2^64-1。

Snowflake 使用的是無符號長整型數字,即從左到右一共 64 位二進位制組成,但其第一位是不使用的。所以,在 Snowflake 中使用的是 63bit 的長整型無符號數字,它們由時間戳、機器號、毫秒內併發序列號三個部分組成 :

·  時間戳位:當前毫秒時間戳與新紀元時間戳的差值(所謂新紀元時間戳就是應用開始使用 Snowflake 的時間。如果不設定新紀元時間,時間戳預設是從1970年開始計算的,設定了新紀元時間可以延長 Snowflake 的可用時間)。41 位 2 進位制轉為十進位制是 2^41,除以(365 天 * 24 小時 * 3600 秒 * 1000 毫秒),約等於 69年,所以最多可以使用 69 年;

·  機器號:10 位 2 進位制轉為十進位制是 2^10,即 1024,也就是說最多可以支援有 1024 個機器節點;

·  毫秒內併發序列號:12 位 2 進位制轉為十進位制是 2^12,即 4096,也就是說一毫秒內在一個機器節點上併發的獲取 ID,最多可以支援 4096 個併發;

下面我們來看一下各個分段的使用情況:

二進位制分段  

[1]  

[2, 42]  

[43, 52]  

[53, 64]  

說明  

最高符號位不使用  

一共41位,是毫秒時間戳位  

一個10位,是機器號位  

一共12位,是毫秒內併發序列號,當前請求的時間戳如果和上一次請求的時間戳相同,那麼就將毫秒內併發序列號加一  

那麼 Snowflake 生成的 ID 長什麼樣子呢?下面我們來舉幾個例子(假設我們的時間戳新紀元是 2020-12-31 00:00:00):

時間

機器號

毫秒併發

十進位制 Snowflake ID

2021-01-01 08:33:11

1

10

491031363588106

2021-01-02 13:11:12

2

25

923887730696217

2021-01-03 21:22:01

3

1

1409793654796289

Snowflake 可以使用三種不同的部署方式來部署,整合分散式部署方式、中心叢集式部署方式、直連叢集式部署方式。下面我們來分別介紹一下這幾種部署方式。

Snowflake 整合分散式部署方式

當使用 ID 的應用節點比較少時,比如 200 個節點以內,適合使用整合分散式部署方式。每個應用節點在啟動的時候決定了機器號後,執行時不依賴任何第三方服務,在本地使用時間戳、機器號、以及毫秒內併發序列號生成 ID。

下圖展示的是應用伺服器透過引入 jar 包的方式實現獲取分散式 ID 的過程。每一個使用分散式 ID 的應用伺服器節點都會分配一個拓撲網路內唯一的機器號。這個機器號的管理存放在 MySQL 或者 ZooKeeper 上。

 

當拓撲網路內使用分散式 ID 的機器節點很多,例如超過 1000 個機器節點時,使用整合部署的分散式 ID 就不合適了,因為機器號位一共是 10 位,即最多支援 1024 個機器號。當機器節點超過 1000 個機器節點時,可以使用下面要介紹的中心叢集式部署方式。

Snowflake 中心叢集式部署方式

中心叢集式部署需要新增用來做請求轉發的 ID 閘道器,比如使用 nginx 反向代理(即下圖中的 ID REST API Gateway)。

使用 ID 閘道器組網後,應用伺服器透過 HTTP 或 RPC 請求 ID 閘道器獲取分散式 ID。這樣相比於上面的整合分散式部署方式,就可以支撐更多的應用節點使用分散式 ID 了。

如圖所示,機器號的分配只是分配給下圖中的 ID Generator node 節點,應用節點是不需要分配機器號的。

 

使用中心叢集式部署方式需要引入新的 nginx 反向代理做閘道器,增加了系統的複雜性,降低了服務的可用性。那麼我們下面再介紹一種不需要引入 nginx 又可以支援超過 1000 個應用節點的直連叢集部署方式。

Snowflake 直連叢集式部署方式

相比於中心叢集部署方式,直連叢集部署方式可以去掉中間的 ID 閘道器,提高服務的可用性。

在使用 ID 閘道器的時候,我們需要把 ID generator node 的服務地址配置在 ID 閘道器中。而在使用直連叢集式部署方式時,ID generator node 的服務地址可以配置在應用伺服器本地配置檔案中,或者配置在配置中心。應用伺服器獲取到服務地址列表後,需要實現服務路由,直連 ID 生成器獲取 ID。

 

Snowflake 演算法存在的問題

Snowflake 演算法是強依賴時間戳的演算法,如果一旦發生時鐘回撥就會產生 ID 重複的問題。那麼時鐘回撥是怎麼產生的,我們又需要怎麼去解決這個問題呢?

NTP(Network Time Protocol)服務自動校準可能導致時鐘回撥。我們身邊的每一臺計算機都有自己本地的時鐘,這個時鐘是根據 CPU 的晶振脈衝計算得來的,然而隨著執行時間的推移,這個時間和世界時間的偏差會越來越大,那麼 NTP 就是用來做時鐘校準的服務。

一般情況下發生時鐘回撥的機率也非常小,因為一旦出現本地時間相對於世界時間需要校準,但時鐘偏差值小於 STEP 閾值(預設128毫秒)時,計算機會選擇以 SLEW 的方式進行同步,即以 0.5 毫秒/秒的速度差調整時鐘速度,保證本地時鐘是一直連續向前的,不產生時鐘回撥,直到本地時鐘和世界時鐘對齊。

然而如果本地時鐘和世界時鐘相差大於 STEP 閾值時,就會發生時鐘回撥。這個 STEP 閾值是可以修改的,但是修改 的越大,在 SLEW 校準的時候需要花費的校準時間就越長,例如 STEP 閾值設定為 10 分鐘,即本地時鐘與世界時鐘偏差在 10 分鐘以內時都會以 SLEW 的方式進行校準,這樣最多會需要 14 天才會完成校準。

為了避免時鐘回撥導致重複 ID 的問題,可以使用 128 毫秒的 STEP 閾值,同時在獲取 SnowflakeID 的時候與上一次的時間戳相比,判斷時鐘回撥是否在 1 秒鐘以內,如果在 1 秒鐘以內,那麼等待 1 秒鐘,否則服務不可用,這樣可以解決時鐘回撥 1 秒鐘的問題。

分段步長方案

Snowflake 由於是將時間戳作為長整形的高位,所以導致生成的最小數字也非常大。比如超過時間新紀元 1 秒鐘,機器號為 1,毫秒併發序列為 1 時,生成的 ID 就已經到 4194308097 了。那麼有沒有一種方法能夠實現在初始狀態生成數字較小的 ID 呢?答案是肯定的,下面來介紹一下分段步長 ID 方案。

使用 分段步長 來生成 ID 就是將步長和當前最大 ID 存在資料庫中,每次獲取 ID 時更新資料庫中的 ID 最大值增加步長。

資料庫核心表結構如下所示:

CREATE   TABLE   `segment_id`  (

  `id`   bigint ( 20 ) NOT   NULL  AUTO_INCREMENT,

  `biz_type`   varchar ( 64 ) NOT   NULL   DEFAULT   '' , // 業務型別

  `max`   bigint ( 20 ) DEFAULT   0' , // 當前最大 ID  

  `step`   bigint ( 20 ) DEFAULT   '10000' , // ID   步長

  PRIMARY KEY  ( `id` )

) ENGINE = InnoDB  AUTO_INCREMENT= 3   DEFAULT   CHARSET =utf8           

在獲取 ID 時,使用開啟事務,利用行鎖保證讀取到當前更新的最大 ID 值:

start   transaction ; update  segment_id set   max  = max  + step where  biz_type = 'ORDER' ; select   max   from  segment_id where  biz_type = 'ORDER' ; commit

分段步長 ID 生成方案的優缺點:

·  優點:ID 生成不依賴時間戳,ID 生成初始值可以從 0 開始逐漸增加;

·  缺點:當服務重啟時需要將最大 ID 值增加步長,頻繁重啟的話就會浪費掉很多分段。

針對上述兩種實現方案的最佳化

上文介紹了 Snowflake 演算法以及分段步長方案,他們各有優缺點,針對他們各自的情況我們在本文也給出相應的最佳化方案。

ID 緩衝環

為了提高  SnowflakeID  的併發效能和可用性,可以使用 ID 緩衝環(即 ID Buffer Ring)。提高併發性提現在透過使用緩衝環能夠充分利用毫秒時間戳,提高可用性提現在可以相對緩解由時鐘回撥導致的服務不可用。緩衝環是透過定長陣列加遊標雜湊實現的,相比於連結串列會不需要頻繁的記憶體分配。

ID 緩衝環初始化的時候會請求 ID 生成器將 ID 緩衝環填滿,當業務需要獲取 ID 時,從緩衝環的頭部依次獲取 ID。當 ID 緩衝環中剩餘的 ID 數量少於設定的閾值百分比時,比如剩餘 ID 數量少於整個 ID 緩衝環的 30% 時,觸發非同步 ID 填充載入。非同步 ID 填充載入會將新生成的 ID 追加到 ID 緩衝環的佇列末尾,然後按照雜湊演算法對映到 ID 緩衝環上。另外有一個單獨的定時器非同步執行緒來定時填充 ID 緩衝環。

下面的動畫展示了 ID 緩衝環的三個階段:ID 初始化載入、ID 消費、ID 消費後填充:

·  Buffer Ring Initialize load,ID 緩衝環初始化載入:從 ID generator 獲取到 ID 填充到 ID 緩衝環,直到 ID 緩衝環被填滿;

·  Buffer Ring consume,ID 緩衝環消費:業務應用從 ID 緩衝環獲取 ID;

·  Async reload,非同步載入填充 ID 緩衝環:定時器執行緒負責非同步的從 ID generator 獲取 ID 新增到 ID 緩衝佇列,同時按照雜湊演算法對映到 ID 緩衝環上,當 ID 緩衝環被填滿時,非同步載入填充結束;

 

下面的流程圖展示了 ID 緩衝環的執行的整個生命週期,其中:

·  IDConsumerServer:表示使用分散式 ID 的業務系統;

·  IDBufferRing:ID 緩衝環;

·  IDGenerator:ID 生成器;

·  IDBufferRingAsyncLoadThread:非同步載入 ID 到緩衝環的執行緒;

·  Timer:負責定時向非同步載入執行緒新增任務來裝載 ID;

·  ID 消費流程:即 上面提到的 Buffer Ring consume;

整體流程:客戶端業務請求到應用伺服器,應用伺服器從 ID 緩衝環獲取 ID,如果 ID 緩衝環內空了那麼丟擲服務不可用;如果 ID 緩衝環記憶體有 ID 那麼就消費一個 ID 。同時在消費 ID 緩衝環中的 ID 時,如果發現 ID 緩衝環中存留的 ID 數量少於整個 ID 緩衝環容量的 30% 時觸發非同步載入填充 ID 緩衝環。

 

ID 雙桶 Buffer

在使用 分段步長 ID  時,如果該分段的 ID 用完了,需要更新資料庫分段最大值再繼續提供 ID 生成服務,為了減少資料庫更新查詢可能帶來的延時對 ID 服務的效能影響,可以使用雙桶快取方案來提高 ID 生成服務的可用性。

其主要原理:設計兩個快取桶:currentBufferBucket 和 nextBufferBucket,每個桶都存放一個步長這麼多的 ID,如果當前快取桶的 ID 用完了,那麼就將下一個快取桶設定為當前快取桶。

下面的動畫展示了雙桶快取初始化、非同步載入預備桶和將預備桶切換成當前桶的全過程:

·  Current bucket initial load:初始化當前的快取桶,即更新 max = max + step,然後獲取更新後的 max 值,比如步長是 1000,更新後的 max 值是 1000,那麼桶的高度就是步長即 1000,桶 min = max - step + 1 = 1,max = 1000;

·  Current bucket remaining id count down to 20%,Next bucket start to load。當前快取桶的 ID 剩餘不足 20% 的時候可以載入下一個快取桶,即更新 max = max + step,後獲取更新後的 max 值,此時更新後的 max 值是 2000,min = max - step + 1 = 1001, max = 2000;

·  Current bucket is drained,Switch current bucket to the next bucket,如果當前桶的 ID 全部用完了,那麼就將下一個 ID 快取桶設定為當前桶;

 

下面是雙桶 Buffer 的流程圖:

 

總結

本文主要介紹了分散式 ID 的實現方案,並詳細介紹了其中 Snowflake 方案和分段步長方案,以及針對這兩種方案的最佳化方案。我們再簡單總結一下兩個方案:

·  在高併發場景下生成大量的分散式 ID,適合使用 Snowflake 演算法方案,毫秒內併發序列為2^12=4096,單機 QPS 支援高達 4 百萬,但是需要對 ID 生成器的機器號進行管理;

·  使用分段步長方式生成 ID 就可以免去對機器號的管理,但是需要合理的設定步長,如果步長太短滿足不了併發需求,如果步長太長又會造成分段的過渡浪費;

以上就是本文的全部內容,如果有更多關於分散式 ID 的技術也歡迎留言與我們交流。

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69989270/viewspace-2753462/,如需轉載,請註明出處,否則將追究法律責任。

相關文章