當前位置: 妍妍網 > 碼農

面試官:為什麽需要分布式ID?你計畫中是怎麽做的?

2024-03-01碼農

架構師(JiaGouX)

我們都是架構師!
架構未來,你來不來?

今天分享一位讀者去京東面試遇到的面試題:「為什麽要分布式 ID?你計畫中是怎麽做的?」。

這篇文章我會說說自己的看法,詳細介紹一下分布式 ID 相關的內容包括分布式 ID 的基本要求以及分布式 ID 常見的解決方案。

內容概覽:

個人能力有限。如果文章有任何需要補充/完善/修改的地方,歡迎在評論區指出,共同進步!

分布式 ID 介紹

什麽是 ID?

日常開發中,我們需要對系統中的各種數據使用 ID 唯一表示,比如使用者 ID 對應且僅對應一個人,商品 ID 對應且僅對應一件商品,訂單 ID 對應且僅對應一個訂單。

我們現實生活中也有各種 ID,比如身份證 ID 對應且僅對應一個人、地址 ID 對應且僅對應

簡單來說, ID 就是數據的唯一標識

什麽是分布式 ID?

分布式 ID 是分布式系統下的 ID。分布式 ID 不存在與現實生活中,屬於電腦系統中的一個概念。

我簡單舉一個分庫分表的例子。

我司的一個計畫,使用的是單機 MySQL 。但是,沒想到的是,計畫上線一個月之後,隨著使用人數越來越多,整個系統的數據量將越來越大。單機 MySQL 已經沒辦法支撐了,需要進行分庫分表(推薦 Sharding-JDBC)。

在分庫之後, 數據遍布在不同伺服器上的資料庫,資料庫的自增主鍵已經沒辦法滿足生成的主鍵唯一了。 我們如何為不同的數據節點生成全域唯一主鍵呢?

這個時候就需要生成 分布式 ID 了。

分布式 ID 需要滿足哪些要求?

分布式 ID 作為分布式系統中必不可少的一環,很多地方都要用到分布式 ID。

一個最基本的分布式 ID 需要滿足下面這些要求:

  • 全域唯一 :ID 的全域唯一性肯定是首先要滿足的!

  • 高效能 :分布式 ID 的生成速度要快,對本地資源消耗要小。

  • 高可用 :生成分布式 ID 的服務要保證可用性無限接近於 100%。

  • 方便易用 :拿來即用,使用方便,快速接入!

  • 除了這些之外,一個比較好的分布式 ID 還應保證:

  • 安全 :ID 中不包含敏感資訊。

  • 有序遞增 :如果要把 ID 存放在資料庫的話,ID 的有序性可以提升資料庫寫入速度。並且,很多時候 ,我們還很有可能會直接透過 ID 來進行排序。

  • 有具體的業務含義 :生成的 ID 如果能有具體的業務含義,可以讓定位問題以及開發更透明化(透過 ID 就能確定是哪個業務)。

  • 獨立部署 :也就是分布式系統單獨有一個發號器服務,專門用來生成分布式 ID。這樣就生成 ID 的服務可以和業務相關的服務解耦。不過,這樣同樣帶來了網路呼叫消耗增加的問題。總的來說,如果需要用到分布式 ID 的場景比較多的話,獨立部署的發號器服務還是很有必要的。

  • 分布式 ID 常見解決方案

    資料庫

    資料庫主鍵自增

    這種方式就比較簡單直白了,就是透過關系型資料庫的自增主鍵產生來唯一的 ID。

    資料庫主鍵自增

    以 MySQL 舉例,我們透過下面的方式即可。

    1.建立一個資料庫表。

    CREATETABLE`sequence_id` (
    `id`bigint(20unsignedNOTNULL AUTO_INCREMENT,
    `stub`char(10NOTNULLDEFAULT'',
    PRIMARY KEY (`id`),
    UNIQUEKEY`stub` (`stub`)
    ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

    stub 欄位無意義,只是為了占位,便於我們插入或者修改數據。並且,給 stub 欄位建立了唯一索引,保證其唯一性。

    2.透過 replace into 來插入數據。

    BEGIN;
    REPLACE INTO sequence_id(stub)VALUES('stub');
    SELECT LAST_INSERT_ID();
    COMMIT;

    插入數據這裏,我們沒有使用 insert into 而是使用 replace into 來插入數據,具體步驟是這樣的:

  • 第一步:嘗試把數據插入到表中。

  • 第二步:如果主鍵或唯一索引欄位出現重復數據錯誤而插入失敗時,先從表中刪除含有重復關鍵字值的沖突行,然後再次嘗試把數據插入到表中。

  • 這種方式的優缺點也比較明顯:

  • 優點 :實作起來比較簡單、ID 有序遞增、儲存消耗空間小

  • 缺點 :支持的並行量不大、存在資料庫單點問題(可以使用資料庫集群解決,不過增加了復雜度)、ID 沒有具體業務含義、安全問題(比如根據訂單 ID 的遞增規律就能推算出每天的訂單量,商業機密啊!)、每次獲取 ID 都要存取一次資料庫(增加了對資料庫的壓力,獲取速度也慢)

  • 資料庫號段模式

    資料庫主鍵自增這種模式,每次獲取 ID 都要存取一次資料庫,ID 需求比較大的時候,肯定是不行的。

    如果我們可以批次獲取,然後存在在記憶體裏面,需要用到的時候,直接從記憶體裏面拿就舒服了!這也就是我們說的 基於資料庫的號段模式來生成分布式 ID。

    資料庫的號段模式也是目前比較主流的一種分布式 ID 生成方式。像滴滴開源的 Tinyid [1] 就是基於這種方式來做的。不過,TinyId 使用了雙號段緩存、增加多 db 支持等方式來進一步最佳化。

    以 MySQL 舉例,我們透過下面的方式即可。

    1. 建立一個資料庫表。

    CREATETABLE`sequence_id_generator` (
    `id`int(10NOTNULL,
    `current_max_id`bigint(20NOTNULLCOMMENT'當前最大id',
    `step`int(10NOTNULLCOMMENT'號段的長度',
    `version`int(20NOTNULLCOMMENT'版本號',
    `biz_type`int(20NOTNULLCOMMENT'業務型別',
    PRIMARY KEY (`id`)
    ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;

    current_max_id 欄位和 step 欄位主要用於獲取批次 ID,獲取的批次 id 為: current_max_id ~ current_max_id+step

    資料庫號段模式

    version 欄位主要用於解決並行問題(樂觀鎖), biz_type 主要用於表示業務型別。

    2. 先插入一行數據。

    INSERTINTO`sequence_id_generator` (`id``current_max_id``step``version``biz_type`)
    VALUES
     (101000101);

    3. 透過 SELECT 獲取指定業務下的批次唯一 ID

    SELECT`current_max_id``step`,`version`FROM`sequence_id_generator`where`biz_type` = 101

    結果:

    id current_max_id step version biz_type
    1 0 100 0 101

    4. 不夠用的話,更新之後重新 SELECT 即可。

    UPDATE sequence_id_generator SET current_max_id = 0+100version=version+1WHEREversion = 0AND`biz_type` = 101
    SELECT`current_max_id``step`,`version`FROM`sequence_id_generator`where`biz_type` = 101

    結果:

    id current_max_id step version biz_type
    1 100 100 1 101

    相比於資料庫主鍵自增的方式, 資料庫的號段模式對於資料庫的存取次數更少,資料庫壓力更小。

    另外,為了避免單點問題,你可以從使用主從模式來提高可用性。

    資料庫號段模式的優缺點:

  • 優點 :ID 有序遞增、儲存消耗空間小

  • 缺點 :存在資料庫單點問題(可以使用資料庫集群解決,不過增加了復雜度)、ID 沒有具體業務含義、安全問題(比如根據訂單 ID 的遞增規律就能推算出每天的訂單量,商業機密啊!)

  • NoSQL

    一般情況下,NoSQL 方案使用 Redis 多一些。我們透過 Redis 的 incr 命令即可實作對 id 原子順序遞增。

    127.0.0.1:6379> set sequence_id_biz_type 1
    OK
    127.0.0.1:6379> incr sequence_id_biz_type
    (integer) 2
    127.0.0.1:6379> get sequence_id_biz_type
    "2"

    為了提高可用性和並行,我們可以使用 Redis Cluster。Redis Cluster 是 Redis 官方提供的 Redis 集群解決方案(3.0+版本)。

    除了 Redis Cluster 之外,你也可以使用開源的 Redis 集群方案 Codis [2] (大規模集群比如上百個節點的時候比較推薦)。

    除了高可用和並行之外,我們知道 Redis 基於記憶體,我們需要持久化數據,避免重新開機機器或者機器故障後數據遺失。Redis 支持兩種不同的持久化方式: 快照(snapshotting,RDB) 只追加檔(append-only file, AOF) 。並且,Redis 4.0 開始支持 RDB 和 AOF 的混合持久化 (預設關閉,可以透過配置項 aof-use-rdb-preamble 開啟)。

    關於 Redis 持久化,我這裏就不過多介紹。不了解這部份內容的小夥伴,可以看看 Redis 持久化機制詳解 [3] 這篇文章。

    Redis 方案的優缺點:

  • 優點 :效能不錯並且生成的 ID 是有序遞增的

  • 缺點 :和資料庫主鍵自增方案的缺點類似

  • 除了 Redis 之外,MongoDB ObjectId 經常也會被拿來當做分布式 ID 的解決方案。

    MongoDB ObjectId 一共需要 12 個字節儲存:

  • 0~3:時間戳

  • 3~6:代表機器 ID

  • 7~8:機器行程 ID

  • 9~11:自增值

  • MongoDB 方案的優缺點:

  • 優點 :效能不錯並且生成的 ID 是有序遞增的

  • 缺點 :需要解決重復 ID 問題(當機器時間不對的情況下,可能導致會產生重復 ID)、有安全性問題(ID 生成有規律性)

  • 演算法

    UUID

    UUID 是 Universally Unique Identifier(通用唯一識別元) 的縮寫。UUID 包含 32 個 16 進制數位(8-4-4-4-12)。

    JDK 就提供了現成的生成 UUID 的方法,一行程式碼就行了。

    //輸出範例:cb4a9ede-fa5e-4585-b9bb-d60bce986eaa
    UUID.randomUUID()

    RFC 4122 [4] 中關於 UUID 的範例是這樣的:

    我們這裏重點關註一下這個 Version(版本),不同的版本對應的 UUID 的生成規則是不同的。

    5 種不同的 Version(版本)值分別對應的含義(參考 維基百科對於 UUID 的介紹 [5] ):

  • 版本 1 : UUID 是根據時間和節點 ID(通常是 MAC 地址)生成;

  • 版本 2 : UUID 是根據識別元(通常是組或使用者 ID)、時間和節點 ID 生成;

  • 版本 3、版本 5 : 版本 5 - 確定性 UUID 透過雜湊(hashing)名字空間(namespace)識別元和名稱生成;

  • 版本 4 : UUID 使用 隨機性 [6] 偽隨機性 [7] 生成。

  • 下面是 Version 1 版本下生成的 UUID 的範例:

    Version 1 版本下生成的 UUID 的範例

    JDK 中透過 UUID randomUUID() 方法生成的 UUID 的版本預設為 4。

    UUID uuid = UUID.randomUUID();
    int version = uuid.version();// 4

    另外,Variant(變體)也有 4 種不同的值,這種值分別對應不同的含義。這裏就不介紹了,貌似平時也不怎麽需要關註。

    需要用到的時候,去看看維基百科對於 UUID 的 Variant(變體) 相關的介紹即可。

    從上面的介紹中可以看出,UUID 可以保證唯一性,因為其生成規則包括 MAC 地址、時間戳、名字空間(Namespace)、隨機或偽隨機數、時序等元素,電腦基於這些規則生成的 UUID 是肯定不會重復的。

    雖然,UUID 可以做到全域唯一性,但是,我們一般很少會使用它。

    比如使用 UUID 作為 MySQL 資料庫主鍵的時候就非常不合適:

  • 資料庫主鍵要盡量越短越好,而 UUID 的消耗的儲存空間比較大(32 個字串,128 位)。

  • UUID 是無順序的,InnoDB 引擎下,資料庫主鍵的無序性會嚴重影響資料庫效能。

  • 最後,我們再簡單分析一下 UUID 的優缺點 (面試的時候可能會被問到的哦!) :

  • 優點 :生成速度比較快、簡單易用

  • 缺點 :儲存消耗空間大(32 個字串,128 位)、 不安全(基於 MAC 地址生成 UUID 的演算法會造成 MAC 地址泄露)、無序(非自增)、沒有具體業務含義、需要解決重復 ID 問題(當機器時間不對的情況下,可能導致會產生重復 ID)

  • Snowflake(雪花演算法)

    Snowflake 是 Twitter 開源的分布式 ID 生成演算法。Snowflake 由 64 bit 的二進制數位組成,這 64bit 的二進制被分成了幾部份,每一部份儲存的數據都有特定的含義:

    Snowflake 組成
  • sign(1bit) :符號位(標識正負),始終為 0,代表生成的 ID 為正數。

  • timestamp (41 bits) :一共 41 位,用來表示時間戳,單位是毫秒,可以支撐 2 ^41 毫秒(約 69 年)

  • datacenter id + worker id (10 bits) :一般來說,前 5 位表示機房 ID,後 5 位表示機器 ID(實際計畫中可以根據實際情況調整)。這樣就可以區分不同集群/機房的節點。

  • sequence (12 bits) :一共 12 位,用來表示序列號。序列號為自增值,代表單台機器每毫秒能夠產生的最大 ID 數(2^12 = 4096),也就是說單台機器每毫秒最多可以生成 4096 個 唯一 ID。

  • 在實際計畫中,我們一般也會對 Snowflake 演算法進行改造,最常見的就是在 Snowflake 演算法生成的 ID 中加入業務型別資訊。

    我們再來看看 Snowflake 演算法的優缺點:

  • 優點 :生成速度比較快、生成的 ID 有序遞增、比較靈活(可以對 Snowflake 演算法進行簡單的改造比如加入業務 ID)

  • 缺點 :需要解決重復 ID 問題(ID 生成依賴時間,在獲取時間的時候,可能會出現時間回撥的問題,也就是伺服器上的時間突然倒退到之前的時間,進而導致會產生重復 ID)、依賴機器 ID 對分布式環境不友好(當需要自動啟停或增減機器時,固定的機器 ID 可能不夠靈活)。

  • 如果你想要使用 Snowflake 演算法的話,一般不需要你自己再造輪子。有很多基於 Snowflake 演算法的開源實作比如美團 的 Leaf、百度的 UidGenerator(後面會提到),並且這些開源實作對原有的 Snowflake 演算法進行了最佳化,效能更優秀,還解決了 Snowflake 演算法的時間回撥問題和依賴機器 ID 的問題。

    並且,Seata 還提出了「改良版雪花演算法」,針對原版雪花演算法進行了一定的最佳化改良,解決了時間回撥問題,大幅提高的 QPS。具體介紹和改進原理,可以參考下面這兩篇文章:

  • Seata 基於改良版雪花演算法的分布式 UUID 生成器分析 [8]

  • 在開源計畫中看到一個改良版的雪花演算法,現在它是你的了。 [9]

  • 開源框架

    UidGenerator(百度)

    UidGenerator [10] 是百度開源的一款基於 Snowflake(雪花演算法)的唯一 ID 生成器。

    不過,UidGenerator 對 Snowflake(雪花演算法)進行了改進,生成的唯一 ID 組成如下:

    UidGenerator 生成的 ID 組成
  • sign(1bit) :符號位(標識正負),始終為 0,代表生成的 ID 為正數。

  • delta seconds (28 bits) :當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,最多可支持約 8.7 年

  • worker id (22 bits) :機器 id,最多可支持約 420w 次機器啟動。內建實作為在啟動時由資料庫分配,預設分配策略為用後即棄,後續可提供復用策略。

  • sequence (13 bits) :每秒下的並行序列,13 bits 可支持每秒 8192 個並行。

  • 可以看出,和原始 Snowflake(雪花演算法)生成的唯一 ID 的組成不太一樣。並且,上面這些參數我們都可以自訂。

    UidGenerator 官方文件中的介紹如下:

    自 18 年後,UidGenerator 就基本沒有再維護了,我這裏也不過多介紹。想要進一步了解的朋友,可以看看 UidGenerator 的官方介紹 [11]

    Leaf(美團)

    Leaf [12] 是美團開源的一個分布式 ID 解決方案 。這個計畫的名字 Leaf(樹葉) 起源於德國哲學家、數學家萊布尼茲的一句話:「There are no two identical leaves in the world」(世界上沒有兩片相同的樹葉) 。這名字起得真心挺不錯的,有點文藝青年那味了!

    Leaf 提供了 號段模式 Snowflake(雪花演算法) 這兩種模式來生成分布式 ID。並且,它支持雙號段,還解決了雪花 ID 系統時鐘回撥問題。不過,時鐘問題的解決需要弱依賴於 Zookeeper(使用 Zookeeper 作為註冊中心,透過在特定路徑下讀取和建立子節點來管理 workId) 。

    Leaf 的誕生主要是為了解決美團各個業務線生成分布式 ID 的方法多種多樣以及不可靠的問題。

    Leaf 對原有的號段模式進行改進,比如它這裏增加了雙號段避免獲取 DB 在獲取號段的時候阻塞請求獲取 ID 的執行緒。簡單來說,就是我一個號段還沒用完之前,我自己就主動提前去獲取下一個號段(圖片來自於美團官方文章: 【Leaf——美團點評分布式 ID 生成系統】 [13] )。

    根據計畫 README 介紹,在 4C8G VM 基礎上,透過公司 RPC 方式呼叫,QPS 壓測結果近 5w/s,TP999 1ms。

    Tinyid(滴滴)

    Tinyid [14] 是滴滴開源的一款基於資料庫號段模式的唯一 ID 生成器。

    資料庫號段模式的原理我們在上面已經介紹過了。 Tinyid 有哪些亮點呢?

    為了搞清楚這個問題,我們先來看看基於資料庫號段模式的簡單架構方案。(圖片來自於 Tinyid 的官方 wiki: 【Tinyid 原理介紹】 [15]

    在這種架構模式下,我們透過 HTTP 請求向發號器服務申請唯一 ID。負載均衡 router 會把我們的請求送往其中的一台 tinyid-server。

    這種方案有什麽問題呢?在我看來(Tinyid 官方 wiki 也有介紹到),主要由下面這 2 個問題:

  • 獲取新號段的情況下,程式獲取唯一 ID 的速度比較慢。

  • 需要保證 DB 高可用,這個是比較麻煩且耗費資源的。

  • 除此之外,HTTP 呼叫也存在網路開銷。

    Tinyid 的原理比較簡單,其架構如下圖所示:

    相比於基於資料庫號段模式的簡單架構方案,Tinyid 方案主要做了下面這些最佳化:

  • 雙號段緩存 :為了避免在獲取新號段的情況下,程式獲取唯一 ID 的速度比較慢。Tinyid 中的號段在用到一定程度的時候,就會去異步載入下一個號段,保證記憶體中始終有可用號段。

  • 增加多 db 支持 :支持多個 DB,並且,每個 DB 都能生成唯一 ID,提高了可用性。

  • 增加 tinyid-client :純本地操作,無 HTTP 請求消耗,效能和可用性都有很大提升。

  • Tinyid 的優缺點這裏就不分析了,結合資料庫號段模式的優缺點和 Tinyid 的原理就能知道。

    IdGenerator(個人)

    和 UidGenerator、Leaf 一樣, IdGenerator [16] 也是一款基於 Snowflake(雪花演算法)的唯一 ID 生成器。

    IdGenerator 有如下特點:

  • 生成的唯一 ID 更短;

  • 相容所有雪花演算法(號段模式或經典模式,大廠或小廠);

  • 原生支持 C#/Java/Go/C/Rust/Python/Node.js/PHP(C 擴充套件)/SQL/ 等語言,並提供多緒安全呼叫動態庫(FFI);

  • 解決了時間回撥問題,支持手工插入新 ID(當業務需要在歷史時間生成新 ID 時,用本演算法的預留位能生成 5000 個每秒);

  • 不依賴外部儲存系統;

  • 預設配置下,ID 可用 71000 年不重復。

  • IdGenerator 生成的唯一 ID 組成如下:

    IdGenerator 生成的 ID 組成
  • timestamp (位數不固定) :時間差,是生成 ID 時的系統時間減去 BaseTime(基礎時間,也稱基點時間、原點時間、紀元時間,預設值為 2020 年) 的總時間差(毫秒單位)。初始為 5bits,隨著執行時間而增加。如果覺得預設值太老,你可以重新設定,不過要註意,這個值以後最好不變。

  • worker id (預設 6 bits) :機器 id,機器碼,最重要參數,是區分不同機器或不同套用的唯一 ID,最大值由 WorkerIdBitLength (預設 6)限定。如果一台伺服器部署多個獨立服務,需要為每個服務指定不同的 WorkerId。

  • sequence (預設 6 bits) :序列數,是每毫秒下的序列數,由參數中的 SeqBitLength (預設 6)限定。增加 SeqBitLength 會讓效能更高,但生成的 ID 也會更長。

  • Java 語言使用範例:https://github.com/yitter/idgenerator/tree/master/Java。

    總結

    透過這篇文章,我基本上已經把最常見的分布式 ID 生成方案都總結了一波。

    除了上面介紹的方式之外,像 ZooKeeper 這類中介軟體也可以幫助我們生成唯一 ID。 沒有銀彈,一定要結合實際計畫來選擇最適合自己的方案。

    不過,本文主要介紹的是分布式 ID 的理論知識。在實際的面試中,面試官可能會結合具體的業務場景來考察你對分布式 ID 的設計,你可以參考這篇文章: 分布式 ID 設計指南 [17] (對於實際工作中分布式 ID 的設計也非常有幫助)。

    參考資料

    [1]

    Tinyid: https://github.com/didi/tinyid/wiki/tinyid原理介绍

    [2]

    Codis: https://github.com/CodisLabs/codis

    [3]

    Redis 持久化機制詳解: https://javaguide.cn/database/redis/redis-persistence.html

    [4]

    RFC 4122: https://tools.ietf.org/html/rfc4122

    [5]

    維基百科對於 UUID 的介紹: https://zh.wikipedia.org/wiki/通用唯一辨識碼

    [6]

    隨機性: https://zh.wikipedia.org/wiki/隨機性

    [7]

    偽隨機性: https://zh.wikipedia.org/wiki/偽隨機性

    [8]

    Seata 基於改良版雪花演算法的分布式 UUID 生成器分析: https://seata.io/zh-cn/blog/seata-analysis-UUID-generator.html

    [9]

    在開源計畫中看到一個改良版的雪花演算法,現在它是你的了。: https://www.cnblogs.com/thisiswhy/p/17611163.html

    [10]

    UidGenerator: https://github.com/baidu/uid-generator

    [11]

    UidGenerator 的官方介紹: https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md

    [12]

    Leaf: https://github.com/Meituan-Dianping/Leaf

    [13]

    【Leaf——美團點評分布式 ID 生成系統】: https://tech.meituan.com/2017/04/21/mt-leaf.html

    [14]

    Tinyid: https://github.com/didi/tinyid

    [15]

    【Tinyid 原理介紹】: https://github.com/didi/tinyid/wiki/tinyid原理介绍

    [16]

    IdGenerator: https://github.com/yitter/IdGenerator

    [17]

    分布式 ID 設計指南: https://javaguide.cn/distributed-system/distributed-id-design.html

    如喜歡本文,請點選右上角,把文章分享到朋友圈
    如有想了解學習的技術點,請留言給若飛安排分享

    因公眾號更改推播規則,請點「在看」並加「星標」 第一時間獲取精彩技術分享

    ·END·

    相關閱讀:

    作者:JavaGuide

    來源:JavaGuide

    版權申明:內容來源網路,僅供學習研究,版權歸原創者所有。如有侵權煩請告知,我們會立即刪除並表示歉意。謝謝!

    架構師

    我們都是架構師!

    關註 架構師(JiaGouX),添加「星標」

    獲取每天技術幹貨,一起成為牛逼架構師

    技術群請 加若飛: 1321113940 進架構師群

    投稿、合作、版權等信箱: [email protected]