淺談分散式 ID 的實踐與應用
在業務系統中很多場景下需要生成不重複的 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/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- 淺談分散式計算的開發與實現(一)分散式
- 淺談分散式計算的開發與實現(1)分散式
- [分散式][分散式鎖]淺談分散式鎖分散式
- TiDB 分散式資料庫在轉轉公司的應用實踐TiDB分散式資料庫
- 月半談(一)redis-分散式鎖與應用Redis分散式
- 談談 django 應用實踐Django
- 蘇寧citus分散式資料庫應用實踐分散式資料庫
- 淺談ORACLE的分散式事務Oracle分散式
- 不知誰看過 《分散式Java應用:基礎與實踐》沒有??分散式Java
- Java併發:分散式應用限流 Redis + Lua 實踐Java分散式Redis
- scrapy分散式淺談+京東示例分散式
- TiDB x 漢口銀行丨分散式資料庫應用實踐TiDB分散式資料庫
- 淺談分散式任務排程系統Celery的設計與實現分散式
- 搞懂分散式技術16:淺談分散式鎖的幾種方案分散式
- 分散式鎖實現原理與最佳實踐分散式
- 分散式 | 淺談 dble 引入 ClickHouse 的配置操作分散式
- 應用安全淺談
- TiDB應用實踐TiDB
- 淺談例外表的應用
- Redis、Zookeeper實現分散式鎖——原理與實踐Redis分散式
- 分散式鎖實踐分散式
- 得物技術淺談深入淺出的Redis分散式鎖Redis分散式
- Flume+Kafka收集Docker容器內分散式日誌應用實踐KafkaDocker分散式
- 應用實踐:如何在分散式快取中使用RT和WT?分散式快取
- 效能優化,實踐淺談優化
- VC列印實踐淺談 (轉)
- 淺談因果推斷與在內容平臺的實踐
- Android快應用實踐Android
- 淺談混合應用的演進
- TiDB 在小米的應用實踐TiDB
- 淺談分散式定時任務之quartz分散式quartz
- 也淺談下分散式儲存要點分散式
- 淺談架構-從傳統走向分散式架構分散式
- SpringBoot魔法堂:應用熱部署實踐與原理淺析Spring Boot熱部署
- 《深入實踐Spring Boot》閱讀筆記:分散式應用開發Spring Boot筆記分散式
- TensorFlow分散式實踐分散式
- 深度學習的應用與實踐深度學習
- 分散式事務與Seate框架(2)——Seata實踐分散式框架