?大家好,我是樹哥。
在復雜的分布式系統(tǒng)中,往往需要對大量的數(shù)據(jù)和消息進行唯一標識,例如:分庫分表的 ID 主鍵、分布式追蹤的請求 ID 等等。
【資料圖】
于是,設計「分布式 ID 發(fā)號器」就成為了一個非常常見的系統(tǒng)設計問題。今天我將帶大家一起學習一下,如何設計一個分布式 ID 發(fā)號器。
文章思維導圖
系統(tǒng)訴求對于業(yè)務系統(tǒng)而言,對于全局唯一 ID 一般有如下幾個需求:
全局唯一。生成的 ID 不能重復,這是最基本的要求,否則在分庫分表的場景下就會造成主鍵沖突。單調(diào)遞增。保證下一個 ID 大于上一個 ID,這樣可以保證寫入數(shù)據(jù)庫的時候是順序?qū)懭?,提高寫入性能?p>對于上面兩個需求來說,第一點是所有系統(tǒng)都要求的。而第二點則并不是所有系統(tǒng)都需要,例如分布式追蹤的請求 ID 就可以不需要單調(diào)遞增。而那些需要存到數(shù)據(jù)庫里作為 ID 逐漸的場景,可能就需要保證全局唯一 ID 是單調(diào)遞增的。此外,我們可能還需要考慮安全方面的問題。如果一個全局唯一 ID 是順序遞增的,那么有可能會造成業(yè)務信息的泄露。例如訂單 ID 每次遞增 1,那么競爭對手直接通過訂單 ID 就可以知道我們每天的訂單數(shù),這對于業(yè)務來說是不可接受的。
對于上述的訴求,現(xiàn)在市面上有非常多的唯一 ID 解決方案,其中最為常見的方案有如下 4 種:
UUID類雪花算法數(shù)據(jù)庫自增主鍵Redis 原子自增UUIDUUID 全稱叫 Universally Unique Identifier,即全局唯一標識符,它是 Java 中自帶的 API。一個標準的 UUID 包含 32 個 16 進制的數(shù)字,以中橫線作為分隔符分為 5 段,每段的長度分別為 8 字符、4 字符、4 字符、4 字符、12 字符,大小為 36 個字符,如下圖所示。一個簡單的 UUID 示例:630e4100-e29b-33d4-a635-246652140000。
UUID 構(gòu)成示意圖
對于 UUID 這種唯一 ID 解決方案,優(yōu)點是沒有外部依賴,純本地生成,因此其性能非常高。但缺點也是非常明顯的:
字段非常長,浪費存儲空間。UUID 一般長度為 36 個字符串,如果作為數(shù)據(jù)庫主鍵存儲,極大地增加索引的存儲空間。
非自增,降低數(shù)據(jù)庫寫入性能。UUID 不是自增的,如果作為數(shù)據(jù)庫主鍵,那么無法實現(xiàn)順序?qū)?,從而會降低?shù)據(jù)庫寫入性能。
沒有業(yè)務含義。UUID 是沒有業(yè)務含義的,我們無法從 UUID 中獲取到任何含義。
因此,對于 UUID 而言,其比較適用于非數(shù)據(jù)庫 ID 存儲的情況,例如生成一個本地的分布式追蹤請求 ID。
類雪花算法
雪花算法(SnowFlake)是 Twitter 開源的分布式 ID 生成算法,其思路是用 64 位來表示一個 ID,并將 64 位分割成 4 個部分,如下圖所示。
雪花算法唯一 ID 構(gòu)成示意圖
第一個部分:1 位。固定為 0,表示為正整數(shù)。二進制中最高位是符號位,1 表示負數(shù),0 表示正數(shù)。ID 都是正整數(shù),所以固定為 0。第二個部分:41 位。表示時間戳,精確到毫秒,可以使用 69 年。時間戳帶有自增屬性。第三個部分:10 位。表示 10 位的機器標識,最多支持 1024 個節(jié)點。此部分也可拆分成 5 位 datacenterId 和 5 位 workerId,datacenterId 表示機房 ID,workerId 表示機器 ID。第四部分:12 位。表示序列化,即一些列的自增 ID,可以支持同一節(jié)點同一毫秒生成最多 4095 個 ID 序號。雪花算法的優(yōu)點是:
有業(yè)務含義,并且可自定義。雪花算法的 ID 每一位都有特殊的含義,我們從 ID 的不同位數(shù)就可以推斷出對應的含義。此外,我們還可根據(jù)自身需要,自行增刪每個部分的位數(shù),從而實現(xiàn)自定義的雪花算法。ID 單調(diào)增加,有利于提高寫入性能。雪花算法的 ID 最后部分是遞增的序列號,因此其生成的 ID 是遞增的,將其作為數(shù)據(jù)庫主鍵 ID 時可以實現(xiàn)順序?qū)懭?,從而提高寫入性能。不依賴第三方系統(tǒng)。雪花算法的生成方式,不依賴第三方系統(tǒng)或中間件,因此其穩(wěn)定性較高。解決了安全問題。雪花算法生成的 ID 是單調(diào)遞增的,但其遞增步長又不是確定的,因此無法從 ID 的差值推斷出生成的數(shù)量,從而可以保護業(yè)務隱私。雪花算法幾乎可以是非常完美了,但它有一個致命的缺點 —— 強依賴機器時間。如果機器上的系統(tǒng)時間回撥,即時間較正常的時間慢,那么就可能會出現(xiàn)發(fā)號重復的情況。
對于這種情況,我們可以在本地維護一個文件,寫入上次的時間戳,隨后與當前時間戳比較。如果當前時間戳小于上次時間戳,說明系統(tǒng)時間出了問題,應該及時處理。
整體而言,雪花算法不僅長度更短,而且還具有業(yè)務含義,在數(shù)據(jù)庫存儲的場景下還能提高寫入性能,因此雪花算法生成分布式唯一 ID 受到了大家的歡迎。
現(xiàn)在許多國內(nèi)大廠的開源發(fā)號器的實現(xiàn),都是在雪花算法的基礎上做改進,例如:百度開源的 UidGenerator、美團開源的 Leaf 等等。這些類雪花算法的核心都是將 64 位進行更合理的劃分,從而使得其更適合自身場景。
數(shù)據(jù)庫自增主鍵說起唯一 ID,我們自然會想起數(shù)據(jù)庫的自增主鍵,因為它就是唯一的。
對于并發(fā)量低的情況下,我們可以直接部署 1 臺機器,每次獲取 ID 的時候就往數(shù)據(jù)庫表插入一條數(shù)據(jù),隨后返回主鍵 ID。
這種方式的好處是非常簡單,實現(xiàn)成本低。此外,生成的唯一 ID 也是單調(diào)自增的,可以滿足數(shù)據(jù)庫寫入性能的要求。
但其缺點也非常明顯,即其強依賴數(shù)據(jù)庫。當數(shù)據(jù)庫異常的時候,會造成整個系統(tǒng)不可用。即使做了高可用切換,主從切換時數(shù)據(jù)同步不一致時,仍然可能造成重復發(fā)號。
另外,由于是單機部署,因此其性能瓶頸限制在單臺 MySQL 機器的讀寫性能上,注定無法承擔起高并發(fā)的業(yè)務場景。
對于上面說到的性能問題,我們可以通過集群部署來解決。而集群部署之后的 ID 沖突問題,我們可以通過設置遞增步長來解決。例如如果我們有 3 臺機器,那么我們就設置遞增步長為 3,每臺機器的 ID 生成策略為:
第 1 臺機器,從 0 開始遞增,步長為 3,生成的 ID 分別是:0、3、6、9 等等。第 2 臺機器,從 1 開始遞增,步長為 3,生成的 ID 分別是:1、4、7、10 等等。第 3 臺機器,從 2 開始遞增,步長為 3,生成的 ID 分別是:2、5、8、11 等等。這種方式解決了集群部署以及 ID 沖突的問題,可以在一定程度上提升并發(fā)訪問的容量。但其缺點也比較明顯:
只能依賴堆機器提高性能。當請求再次增多時,我們只能無限堆機器,這貌似是一種物理防御一樣。
水平擴展困難。當我們需要增加一臺機器時,其處理過程非常麻煩。首先,我們需要先把新增的服務器部署好,設置新的步長,起始值要設置一個不可能達到的值。當把新增的服務器部署好之后,再一臺臺處理舊的服務器,這個過程真的非常痛苦,可以說是人肉運維了。
Redis 原子自增由于 Redis 是內(nèi)存數(shù)據(jù)庫,其強大的性能非常適合用來實現(xiàn)高并發(fā)的分布式 ID 生成。基于 Redis 實現(xiàn)自增 ID,其主要還是利用了 Redis 中的 INCR 命令。該命令可以將某個數(shù)自增一并返回結(jié)果,并且這個操作是原子操作。
通過 Redis 實現(xiàn)分布式 ID 功能,其模式與通過數(shù)據(jù)庫自增 ID 類似,只是存儲介質(zhì)從硬盤變成了內(nèi)存。當單臺 Redis 無法支撐并發(fā)請求的時候,Redis 同樣可以通過集群部署和設置步長的方式去解決。
但數(shù)據(jù)庫自增主鍵有的問題,Redis 自增 ID 的方式也同樣會有,即只能堆機器,同時水平擴展困難。此外,比起數(shù)據(jù)庫存儲的持久化,Redis 是基于內(nèi)存的存儲,需要考慮持久化的問題,這同樣是一個頭疼的問題。
總結(jié)看了這么多個分布式 ID 的解決方案,那么我們到底應該選哪個呢?
當我們在決策的時候,我們應該確定決策的維度。對于這個問題,我們應該關注的維度大致有:研發(fā)成本、并發(fā)量、性能、運維成本。
首先,對于 UUID 而言,其在各個方面其實都不如雪花算法,唯一的優(yōu)點是 JDK 自帶 API。因此,如果你只是極其簡單地使用,那么就直接使用 UUID 就可以,畢竟雪花算法還得寫一寫實現(xiàn)代碼呢。
其次,對于類雪花算法而言,其毋庸置疑是非常好的一種實現(xiàn)。與 UUID 相比,其不僅有 UUID 本地生成、不依賴第三方系統(tǒng)的優(yōu)點,還有業(yè)務含義、能提高寫入性能、解決了安全問題。但其缺點在于要實現(xiàn)雪花算法的代碼,因此其研發(fā)成本稍稍比 UUID 高一些。
最后,對于數(shù)據(jù)庫自增 ID 與 Redis 原子自增這兩種方式。數(shù)據(jù)庫自增 ID 的方式,其優(yōu)點同樣在于簡單方便,不需要太高的研發(fā)成本。但其缺點是支撐的并發(fā)量太低,并且后續(xù)運維成本太高。因此,數(shù)據(jù)庫自增 ID 這種方式,應該適用于小規(guī)模的使用場景下。而 Redis 原子自增的方式,其優(yōu)先在于能支撐高并發(fā)的場景。但缺點是需要自行處理持久化問題,運維成本可能比較高。
本人更傾向于數(shù)據(jù)庫自增方式。這兩種方式都是非常類似的,唯一的區(qū)別是存儲介質(zhì)。Redis 原子自增方式非???,可能單機可以是數(shù)據(jù)庫方式的好幾倍。但是如果要考慮持久化的問題,那對于 Redis 來說就太復雜了。
我們把上面這四種實現(xiàn)方式整理一下,可以匯總成下面的對比表格:
實現(xiàn)方案 | 優(yōu)點 | 缺點 | 使用場景 |
UUID | 性能高、不依賴第三方、研發(fā)成本低 | 字段長浪費存儲空間、ID 不自增寫入性能差、無業(yè)務含義 | 非常簡單的使用場景(用于簡單測試) |
類雪花算法 | 有業(yè)務含義、單調(diào)遞增寫入性能好、不依賴第三方、業(yè)務安全 | 強依賴機器時間 | 高并發(fā)、業(yè)務場景復雜、需要將 ID 暴露給外部系統(tǒng) |
數(shù)據(jù)庫自增 ID | 研發(fā)成本低、單調(diào)遞增寫入性能好 | 依賴數(shù)據(jù)庫、只能堆機器提高性能、維護成本高 | 對持久性有要求,不暴露給外部系統(tǒng) |
Redis 原子自增 | 高并發(fā)、單調(diào)遞增寫入性能好 | 依賴 Redis、有業(yè)務問題、有持久性問題 | 對持久性沒要求,不暴露給外部系統(tǒng) |
總的來說,如果站在長期使用考慮,那么運維成本、高并發(fā)肯定是需要考慮的。在這個基礎條件下,類雪花算法與數(shù)據(jù)庫自增 ID 或許是相對好的選擇。
參考資料分布式系統(tǒng)全局發(fā)號器的幾點思考 - 掘金VIP!!非常好!Leaf—— 美團點評分布式 ID 生成系統(tǒng) - 美團技術團隊(8 條消息) 六種方式實現(xiàn)全局唯一發(fā)號器_北鶴 M 的博客 - CSDN 博客_發(fā)號器VIP??!不錯,擴展視野!10 | 發(fā)號器:如何保證分庫分表后 ID 的全局唯一性??標簽:
- 速訊:如何設計一個分布式 ID 發(fā)號器?
- 全球短訊!通俗易懂圖解網(wǎng)絡面試知識—第一篇
- 全面進化!機械師創(chuàng)物者X14高性能創(chuàng)作筆記本曝光
- 【時快訊】我有七種實現(xiàn)Web實時消息推送的方案
- 環(huán)球熱資訊!流量控制(流控)| 深入淺出MGR
- 全球速看:必知必會,七張圖輕松理解 Kubernetes 集群內(nèi)服務通信
- 當前消息!震驚!網(wǎng)絡還可以易容嗎?
- 天天熱點評!繞開5G直奔6G,俄做了一個“難以置信”的決定
- 世界短訊!HTTP 中的常用狀態(tài)碼及使用場景
- 當前簡訊:SDA全景深運維策略加速提升客戶運維能力