【高併發】之分散式全域性唯一 ID

王廷雲的部落格發表於2020-10-13

一、分散式全域性唯一 ID 問題


1、為什麼需要分散式全域性唯一 ID 以及分散式 ID 的業務需求

在複雜的分散式系統中,往往需要對大量的資料和訊息進行唯一標識,如在美團點評的金融、支付、餐飲、酒店,貓眼電影等產品的系統中資料日益增長,對資料分庫分表後需要有一個唯一 ID 來標識一條資料或訊息;特別一點的如訂單、騎手、優惠券也需要有唯一 ID 做標識,此時,一個能夠生成全域性唯一 ID 的系統是非常有必要的

2、ID 生成規則部分硬性要求

  1. 全域性唯一:不能出現重複的 ID 號,既然是唯一標識,那麼這就是最基本的要求
  2. 趨勢遞增:在 MySQL 的 innoDB 引擎中使用的是聚集索引,由於多數 RDBMS 使用 Btree 的資料結構來儲存索引資料,在主鍵的選擇上面我們應該儘量使用有序的主鍵保證寫入效能
  3. 單調遞增:保證下一個 ID 大於上一個 ID,例如事務版本號、IM 增量資訊、排序等特殊需求
  4. 資訊保安:如果 ID 是連續的,惡意使用者的扒取工作就非常容易做了,直接按照順序下載指定 URL 即可,所以在一些應用場景下,需要ID無規則,不規則讓競爭對手不好猜
  5. 含時間戳:這樣就能在開發中快速瞭解分散式 ID 的生成時間

3、ID 號生成系統的可用性要求

  • 高可用:發一個獲取分散式 ID 的請求,伺服器就要保證 99.999% 的情況下給我建立一個唯一分散式 ID
  • 低延遲:發一個獲取分散式ID的請求,伺服器就要快,極速
  • 搞QPS:假如併發一口氣建立分散式 ID 請求同時殺過來,伺服器要頂得住且一下子成功建立10萬

二、一般通用方案


1、UUID

UUID(Universally Unique Identity)的標準型式包含32個16進位制數字,一連字號分為五段,形式為 8-4-4-4-12 的36個字元,比如:560e8609-e78b-68d4-716b-223344660000

其特點是:效能非常高,本地生成,沒有網路消耗。如果只考慮唯一性是沒問題的。

但是由於其隨機性的原因會導致入資料庫效能差:

  • 1、無序,無法預測它的生成順序,不能成功遞增有序的數字
  • 2、主鍵, ID 作為主鍵時在特定的環境會存在一些問題,比如在 DB 主鍵的場景下,MySQL 官方就有明確的建議主鍵儘量越短越好,36個字元長度的 UUID 不符合要求
  • 3、索引,B+ 數索引的分裂:因為 UUID 資料是無序的,所以每一次 UUID 資料的插入都會對主鍵地域的 B+ 樹進行很大的修改,這一點很不好。插入完全無序,不但會導致一些中間節點產生分裂,也會白白創造很多不飽和的節點,這樣大大降低了資料庫插入的效能。

2、資料庫自增主鍵

在分散式裡面,資料庫的自增 ID 機制主要原理是:資料庫自增 ID 是通過 MySQL 資料庫的 replace into 實現的

這裡的 replace into 和 insert into 功能類似,不同點在於:replace into 首先嚐試插入資料列表中,如果發現表中已經有此行資料(根據主鍵或唯一索引判斷)則先刪除,在插入,否則直接插入新資料。

但資料庫自增 ID 機制仍然不適合分散式 ID:

  • 1、系統水平擴充套件比較困難:比如定義好了步長和機器臺數之後,如果要新增機器該怎麼做?假設現在只有一臺機器起始號是 1,2,3,4,5,6 (步長是1),這個時候需要擴容一臺機器一臺。可以這樣做:把第二臺機器的初始值設定得比第一臺超過很多,貌似還好,現在想象一下,如果我們線上有 100 臺機器,這個時候要擴容該怎麼辦?簡直是噩夢。所以系統水平擴充套件方案複雜難以實現。
  • 2、資料庫壓力還是很大,每次獲取 ID 都得讀寫一次資料庫,非常影響效能,不符合分散式 ID 裡面的低延遲和高 QPS 的規則(在高併發下,如果都區別資料庫裡面獲取 ID,那是非常影響效能的)。

3、基於 Redis 生成全域性 ID 策略

因為 Redis 是單執行緒的,天生保證原子性,可以使用原子操作 INCR 和 INCRBY 來實現。

但要注意:在 Redis 叢集情況下,同樣和 MySQL 一樣需要設定不同的增長步長,同時 key 一定要設定有效期。

所以,可以使用 Redis 叢集來獲取更高的吞吐量,假如一個叢集中有5臺Redis,可以初始化每臺 Redis 的值分別為 1,2,3,4,5,然後步長都是5,各個 Redis 生成的 ID 為:

  • A:1,6,11,16,21
  • B:2,7,12,17,22
  • C:3,8,13,18,23
  • D:4,9,14,19,24
  • E:5,10,15,20,25

雖然使用 Redis 可以解決高吞吐量的問題,但為了一個 ID 問題居然要額外維護一個 Redis 叢集,顯然,這是不切實際的。


三、雪花演算法 SnowFlake


1、SnowFlake 簡介

雪花演算法 SnowFlake 是 Twitter 產品的分散式自增 ID 演算法,具體原始碼可以參考官網:https://github.com/twitter-archive/snowflake

Twitter 的分散式雪花演算法 SnowFlake ,經測試每秒能夠生成26萬個自增可排序的 ID

  • 1、生成的 ID 能夠按照時間有序生成
  • 2、演算法生成 ID 的結果是一個64位大小的整數,為一個 Long 型(轉換成字串後長度最多19個字元)
  • 3、分散式系統內不會產生 ID 碰撞(有 datacenter 和 workerId 作區分)並且效率較高

SnowFlake演算法產生的ID是一個64位的整型,結構如下(每一部分用“-”符號分隔):

0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000

1位標識部分,在java中由於long的最高位是符號位,正數是0,負數是1,一般生成的ID為正數,所以為0;

41位時間戳部分,這個是毫秒級的時間,一般實現上不會儲存當前的時間戳,而是時間戳的差值(當前時間-固定的開始時間),這樣可以使產生的ID從更小值開始;41位的時間戳可以使用69年,(1L << 41) / (1000L 60 60 24 365) = 69年;

10位節點部分,Twitter實現中使用前5位作為資料中心標識,後5位作為機器標識,可以部署1024個節點;

12位序列號部分,支援同一毫秒內同一個節點可以生成4096個ID;

SnowFlake演算法生成的ID大致上是按照時間遞增的,用在分散式系統中時,需要注意資料中心標識和機器標識必須唯一,這樣就能保證每個節點生成的ID都是唯一的。或許我們不一定都需要像上面那樣使用5位作為資料中心標識,5位作為機器標識,可以根據我們業務的需要,靈活分配節點部分,如:若不需要資料中心,完全可以使用全部10位作為機器標識;若資料中心不多,也可以只使用3位作為資料中心,7位作為機器標識。

snowflake生成的ID整體上按照時間自增排序,並且整個分散式系統內不會產生ID碰撞(由datacenter和workerId作區分),並且效率較高。

2、SnowFlake 工程落地經驗

糊塗工具包就包含了 SnowFlake 的應用:

  • https://github.com/looly/hutool
  • https://hutool.cn/

SpringBoot 整合雪花演算法:

<dependency>
    <groupId>cn.hutool</groupId>
    <artifactId>hutool-captcha</artifactId>
    <version>5.2.0</version>
</dependency>

相關文章