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