?大家好,我是樹哥。

在復(fù)雜的分布式系統(tǒng)中,往往需要對大量的數(shù)據(jù)和消息進(jìn)行唯一標(biāo)識(shí),例如:分庫分表的 ID 主鍵、分布式追蹤的請求 ID 等等。


【資料圖】

于是,設(shè)計(jì)「分布式 ID 發(fā)號器」就成為了一個(gè)非常常見的系統(tǒng)設(shè)計(jì)問題。今天我將帶大家一起學(xué)習(xí)一下,如何設(shè)計(jì)一個(gè)分布式 ID 發(fā)號器。

文章思維導(dǎo)圖

系統(tǒng)訴求

對于業(yè)務(wù)系統(tǒng)而言,對于全局唯一 ID 一般有如下幾個(gè)需求:

全局唯一。生成的 ID 不能重復(fù),這是最基本的要求,否則在分庫分表的場景下就會(huì)造成主鍵沖突。單調(diào)遞增。保證下一個(gè) ID 大于上一個(gè) ID,這樣可以保證寫入數(shù)據(jù)庫的時(shí)候是順序?qū)懭耄岣邔懭胄阅堋?p>對于上面兩個(gè)需求來說,第一點(diǎn)是所有系統(tǒng)都要求的。而第二點(diǎn)則并不是所有系統(tǒng)都需要,例如分布式追蹤的請求 ID 就可以不需要單調(diào)遞增。而那些需要存到數(shù)據(jù)庫里作為 ID 逐漸的場景,可能就需要保證全局唯一 ID 是單調(diào)遞增的。

此外,我們可能還需要考慮安全方面的問題。如果一個(gè)全局唯一 ID 是順序遞增的,那么有可能會(huì)造成業(yè)務(wù)信息的泄露。例如訂單 ID 每次遞增 1,那么競爭對手直接通過訂單 ID 就可以知道我們每天的訂單數(shù),這對于業(yè)務(wù)來說是不可接受的。

對于上述的訴求,現(xiàn)在市面上有非常多的唯一 ID 解決方案,其中最為常見的方案有如下 4 種:

UUID類雪花算法數(shù)據(jù)庫自增主鍵Redis 原子自增UUID

UUID 全稱叫 Universally Unique Identifier,即全局唯一標(biāo)識(shí)符,它是 Java 中自帶的 API。一個(gè)標(biāo)準(zhǔn)的 UUID 包含 32 個(gè) 16 進(jìn)制的數(shù)字,以中橫線作為分隔符分為 5 段,每段的長度分別為 8 字符、4 字符、4 字符、4 字符、12 字符,大小為 36 個(gè)字符,如下圖所示。一個(gè)簡單的 UUID 示例:630e4100-e29b-33d4-a635-246652140000。

UUID 構(gòu)成示意圖

對于 UUID 這種唯一 ID 解決方案,優(yōu)點(diǎn)是沒有外部依賴,純本地生成,因此其性能非常高。但缺點(diǎn)也是非常明顯的:

字段非常長,浪費(fèi)存儲(chǔ)空間。UUID 一般長度為 36 個(gè)字符串,如果作為數(shù)據(jù)庫主鍵存儲(chǔ),極大地增加索引的存儲(chǔ)空間。

非自增,降低數(shù)據(jù)庫寫入性能。UUID 不是自增的,如果作為數(shù)據(jù)庫主鍵,那么無法實(shí)現(xiàn)順序?qū)懀瑥亩鴷?huì)降低數(shù)據(jù)庫寫入性能。

沒有業(yè)務(wù)含義。UUID 是沒有業(yè)務(wù)含義的,我們無法從 UUID 中獲取到任何含義。

因此,對于 UUID 而言,其比較適用于非數(shù)據(jù)庫 ID 存儲(chǔ)的情況,例如生成一個(gè)本地的分布式追蹤請求 ID。

類雪花算法

雪花算法(SnowFlake)是 Twitter 開源的分布式 ID 生成算法,其思路是用 64 位來表示一個(gè) ID,并將 64 位分割成 4 個(gè)部分,如下圖所示。

雪花算法唯一 ID 構(gòu)成示意圖

第一個(gè)部分:1 位。固定為 0,表示為正整數(shù)。二進(jìn)制中最高位是符號位,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 序號。

雪花算法的優(yōu)點(diǎn)是:

有業(yè)務(wù)含義,并且可自定義。雪花算法的 ID 每一位都有特殊的含義,我們從 ID 的不同位數(shù)就可以推斷出對應(yīng)的含義。此外,我們還可根據(jù)自身需要,自行增刪每個(gè)部分的位數(shù),從而實(shí)現(xiàn)自定義的雪花算法。ID 單調(diào)增加,有利于提高寫入性能。雪花算法的 ID 最后部分是遞增的序列號,因此其生成的 ID 是遞增的,將其作為數(shù)據(jù)庫主鍵 ID 時(shí)可以實(shí)現(xiàn)順序?qū)懭耄瑥亩岣邔懭胄阅堋2灰蕾嚨谌较到y(tǒng)。雪花算法的生成方式,不依賴第三方系統(tǒng)或中間件,因此其穩(wěn)定性較高。解決了安全問題。雪花算法生成的 ID 是單調(diào)遞增的,但其遞增步長又不是確定的,因此無法從 ID 的差值推斷出生成的數(shù)量,從而可以保護(hù)業(yè)務(wù)隱私。

雪花算法幾乎可以是非常完美了,但它有一個(gè)致命的缺點(diǎn) —— 強(qiáng)依賴機(jī)器時(shí)間。如果機(jī)器上的系統(tǒng)時(shí)間回?fù)埽磿r(shí)間較正常的時(shí)間慢,那么就可能會(huì)出現(xiàn)發(fā)號重復(fù)的情況。

對于這種情況,我們可以在本地維護(hù)一個(gè)文件,寫入上次的時(shí)間戳,隨后與當(dāng)前時(shí)間戳比較。如果當(dāng)前時(shí)間戳小于上次時(shí)間戳,說明系統(tǒng)時(shí)間出了問題,應(yīng)該及時(shí)處理。

整體而言,雪花算法不僅長度更短,而且還具有業(yè)務(wù)含義,在數(shù)據(jù)庫存儲(chǔ)的場景下還能提高寫入性能,因此雪花算法生成分布式唯一 ID 受到了大家的歡迎。

現(xiàn)在許多國內(nèi)大廠的開源發(fā)號器的實(shí)現(xiàn),都是在雪花算法的基礎(chǔ)上做改進(jìn),例如:百度開源的 UidGenerator、美團(tuán)開源的 Leaf 等等。這些類雪花算法的核心都是將 64 位進(jìn)行更合理的劃分,從而使得其更適合自身場景。

數(shù)據(jù)庫自增主鍵

說起唯一 ID,我們自然會(huì)想起數(shù)據(jù)庫的自增主鍵,因?yàn)樗褪俏ㄒ坏摹?/p>

對于并發(fā)量低的情況下,我們可以直接部署 1 臺(tái)機(jī)器,每次獲取 ID 的時(shí)候就往數(shù)據(jù)庫表插入一條數(shù)據(jù),隨后返回主鍵 ID。

這種方式的好處是非常簡單,實(shí)現(xiàn)成本低。此外,生成的唯一 ID 也是單調(diào)自增的,可以滿足數(shù)據(jù)庫寫入性能的要求。

但其缺點(diǎn)也非常明顯,即其強(qiáng)依賴數(shù)據(jù)庫。當(dāng)數(shù)據(jù)庫異常的時(shí)候,會(huì)造成整個(gè)系統(tǒng)不可用。即使做了高可用切換,主從切換時(shí)數(shù)據(jù)同步不一致時(shí),仍然可能造成重復(fù)發(fā)號。

另外,由于是單機(jī)部署,因此其性能瓶頸限制在單臺(tái) MySQL 機(jī)器的讀寫性能上,注定無法承擔(dān)起高并發(fā)的業(yè)務(wù)場景。

對于上面說到的性能問題,我們可以通過集群部署來解決。而集群部署之后的 ID 沖突問題,我們可以通過設(shè)置遞增步長來解決。例如如果我們有 3 臺(tái)機(jī)器,那么我們就設(shè)置遞增步長為 3,每臺(tái)機(jī)器的 ID 生成策略為:

第 1 臺(tái)機(jī)器,從 0 開始遞增,步長為 3,生成的 ID 分別是:0、3、6、9 等等。第 2 臺(tái)機(jī)器,從 1 開始遞增,步長為 3,生成的 ID 分別是:1、4、7、10 等等。第 3 臺(tái)機(jī)器,從 2 開始遞增,步長為 3,生成的 ID 分別是:2、5、8、11 等等。

這種方式解決了集群部署以及 ID 沖突的問題,可以在一定程度上提升并發(fā)訪問的容量。但其缺點(diǎn)也比較明顯:

只能依賴堆機(jī)器提高性能。當(dāng)請求再次增多時(shí),我們只能無限堆機(jī)器,這貌似是一種物理防御一樣。

水平擴(kuò)展困難。當(dāng)我們需要增加一臺(tái)機(jī)器時(shí),其處理過程非常麻煩。首先,我們需要先把新增的服務(wù)器部署好,設(shè)置新的步長,起始值要設(shè)置一個(gè)不可能達(dá)到的值。當(dāng)把新增的服務(wù)器部署好之后,再一臺(tái)臺(tái)處理舊的服務(wù)器,這個(gè)過程真的非常痛苦,可以說是人肉運(yùn)維了。

Redis 原子自增

由于 Redis 是內(nèi)存數(shù)據(jù)庫,其強(qiáng)大的性能非常適合用來實(shí)現(xiàn)高并發(fā)的分布式 ID 生成。基于 Redis 實(shí)現(xiàn)自增 ID,其主要還是利用了 Redis 中的 INCR 命令。該命令可以將某個(gè)數(shù)自增一并返回結(jié)果,并且這個(gè)操作是原子操作。

通過 Redis 實(shí)現(xiàn)分布式 ID 功能,其模式與通過數(shù)據(jù)庫自增 ID 類似,只是存儲(chǔ)介質(zhì)從硬盤變成了內(nèi)存。當(dāng)單臺(tái) Redis 無法支撐并發(fā)請求的時(shí)候,Redis 同樣可以通過集群部署和設(shè)置步長的方式去解決。

但數(shù)據(jù)庫自增主鍵有的問題,Redis 自增 ID 的方式也同樣會(huì)有,即只能堆機(jī)器,同時(shí)水平擴(kuò)展困難。此外,比起數(shù)據(jù)庫存儲(chǔ)的持久化,Redis 是基于內(nèi)存的存儲(chǔ),需要考慮持久化的問題,這同樣是一個(gè)頭疼的問題。

總結(jié)

看了這么多個(gè)分布式 ID 的解決方案,那么我們到底應(yīng)該選哪個(gè)呢?

當(dāng)我們在決策的時(shí)候,我們應(yīng)該確定決策的維度。對于這個(gè)問題,我們應(yīng)該關(guān)注的維度大致有:研發(fā)成本、并發(fā)量、性能、運(yùn)維成本。

首先,對于 UUID 而言,其在各個(gè)方面其實(shí)都不如雪花算法,唯一的優(yōu)點(diǎn)是 JDK 自帶 API。因此,如果你只是極其簡單地使用,那么就直接使用 UUID 就可以,畢竟雪花算法還得寫一寫實(shí)現(xiàn)代碼呢。

其次,對于類雪花算法而言,其毋庸置疑是非常好的一種實(shí)現(xiàn)。與 UUID 相比,其不僅有 UUID 本地生成、不依賴第三方系統(tǒng)的優(yōu)點(diǎn),還有業(yè)務(wù)含義、能提高寫入性能、解決了安全問題。但其缺點(diǎn)在于要實(shí)現(xiàn)雪花算法的代碼,因此其研發(fā)成本稍稍比 UUID 高一些。

最后,對于數(shù)據(jù)庫自增 ID 與 Redis 原子自增這兩種方式。數(shù)據(jù)庫自增 ID 的方式,其優(yōu)點(diǎn)同樣在于簡單方便,不需要太高的研發(fā)成本。但其缺點(diǎn)是支撐的并發(fā)量太低,并且后續(xù)運(yùn)維成本太高。因此,數(shù)據(jù)庫自增 ID 這種方式,應(yīng)該適用于小規(guī)模的使用場景下。而 Redis 原子自增的方式,其優(yōu)先在于能支撐高并發(fā)的場景。但缺點(diǎn)是需要自行處理持久化問題,運(yùn)維成本可能比較高。

本人更傾向于數(shù)據(jù)庫自增方式。這兩種方式都是非常類似的,唯一的區(qū)別是存儲(chǔ)介質(zhì)。Redis 原子自增方式非常快,可能單機(jī)可以是數(shù)據(jù)庫方式的好幾倍。但是如果要考慮持久化的問題,那對于 Redis 來說就太復(fù)雜了。

我們把上面這四種實(shí)現(xiàn)方式整理一下,可以匯總成下面的對比表格:

實(shí)現(xiàn)方案

優(yōu)點(diǎn)

缺點(diǎn)

使用場景

UUID

性能高、不依賴第三方、研發(fā)成本低

字段長浪費(fèi)存儲(chǔ)空間、ID 不自增寫入性能差、無業(yè)務(wù)含義

非常簡單的使用場景(用于簡單測試)

類雪花算法

有業(yè)務(wù)含義、單調(diào)遞增寫入性能好、不依賴第三方、業(yè)務(wù)安全

強(qiáng)依賴機(jī)器時(shí)間

高并發(fā)、業(yè)務(wù)場景復(fù)雜、需要將 ID 暴露給外部系統(tǒng)

數(shù)據(jù)庫自增 ID

研發(fā)成本低、單調(diào)遞增寫入性能好

依賴數(shù)據(jù)庫、只能堆機(jī)器提高性能、維護(hù)成本高

對持久性有要求,不暴露給外部系統(tǒng)

Redis 原子自增

高并發(fā)、單調(diào)遞增寫入性能好

依賴 Redis、有業(yè)務(wù)問題、有持久性問題

對持久性沒要求,不暴露給外部系統(tǒng)

總的來說,如果站在長期使用考慮,那么運(yùn)維成本、高并發(fā)肯定是需要考慮的。在這個(gè)基礎(chǔ)條件下,類雪花算法與數(shù)據(jù)庫自增 ID 或許是相對好的選擇。

參考資料分布式系統(tǒng)全局發(fā)號器的幾點(diǎn)思考 - 掘金VIP!!非常好!Leaf—— 美團(tuán)點(diǎn)評分布式 ID 生成系統(tǒng) - 美團(tuán)技術(shù)團(tuán)隊(duì)(8 條消息) 六種方式實(shí)現(xiàn)全局唯一發(fā)號器_北鶴 M 的博客 - CSDN 博客_發(fā)號器VIP!!不錯(cuò),擴(kuò)展視野!10 | 發(fā)號器:如何保證分庫分表后 ID 的全局唯一性??

標(biāo)簽: