唯一ID生成算法剖析

在業(yè)務(wù)開發(fā)中,大量場景需要唯一ID來進(jìn)行標(biāo)識(shí):用戶需要唯一身份標(biāo)識(shí);商品需要唯一標(biāo)識(shí);消息需要唯一標(biāo)識(shí);事件需要唯一標(biāo)識(shí)...等等,都需要全局唯一ID,尤其是分布式場景下。

唯一ID有哪些特性或者說要求呢?按照我的分析有以下特性:

  • 唯一性:生成的ID全局唯一,在特定范圍內(nèi)沖突概率極小
  • 有序性:生成的ID按某種規(guī)則有序,便于數(shù)據(jù)庫插入及排序
  • 可用性:可保證高并發(fā)下的可用性
  • 自主性:分布式環(huán)境下不依賴中心認(rèn)證即可自行生成ID
  • 安全性:不暴露系統(tǒng)和業(yè)務(wù)的信息

一般來說,常用的唯一ID生成方法有這些:

  • UUID:
    • 基于時(shí)間戳&時(shí)鐘序列生成
    • 基于名字空間/名字的散列值(MD5/SHA1)生成
    • 基于隨機(jī)數(shù)生成
  • 數(shù)據(jù)庫自增ID:
    • 多臺(tái)機(jī)器不同初始值、同步長自增
    • 批量緩存自增ID
  • 雪花算法
    • 時(shí)鐘回?fù)芙鉀Q方案

本文便分別對這些算法進(jìn)行講解及分析。


UUID

UUID全稱為:Universally Unique IDentifier(通用唯一識(shí)別碼),有的地方也稱作GUID(Globally Unique IDentifier),實(shí)際上GUID指微軟對于UUID標(biāo)準(zhǔn)的實(shí)現(xiàn)的實(shí)現(xiàn)。

UUID算法的目的是為了生成某種形式的全局唯一ID來標(biāo)識(shí)系統(tǒng)中的任一元素,尤其在分布式環(huán)境下,該ID需要不依賴中心認(rèn)證即可自動(dòng)生成全局唯一ID。

其優(yōu)勢有:

  • 無需網(wǎng)絡(luò),單機(jī)自行生成
  • 速度快,QPS高(支持100ns級(jí)并發(fā))
  • 各語言均有相應(yīng)實(shí)現(xiàn)庫供直接使用

而缺點(diǎn)為:

  • String存儲(chǔ),占空間,DB查詢及索引效率低
  • 無序,可讀性差
  • 根據(jù)實(shí)現(xiàn)方式不同可能泄露信息

1.UUID的格式

UUID的標(biāo)準(zhǔn)形式為32個(gè)十六進(jìn)制數(shù)組成的字符串,且分隔為五個(gè)部分,如:

467e8542-2275-4163-95d6-7adc205580a9

各部分的數(shù)字個(gè)數(shù)為:8-4-4-4-12

2.UUID版本

根據(jù)需要不同,標(biāo)準(zhǔn)提供了不同的UUID版本以供使用,分別對應(yīng)于不同的UUID生成規(guī)則:

  • 版本1 - 基于時(shí)間的UUID:主要依賴當(dāng)前的時(shí)間戳及機(jī)器mac地址,因此可以保證全球唯一性
  • 版本2 - 分布式安全的UUID:將版本1的時(shí)間戳前四位換為POSIX的UID或GID,很少使用
  • 版本3 - 基于名字空間的UUID(MD5版):基于指定的名字空間/名字生成MD5散列值得到,標(biāo)準(zhǔn)不推薦
  • 版本4 - 基于隨機(jī)數(shù)的UUID:基于隨機(jī)數(shù)或偽隨機(jī)數(shù)生成,
  • 版本5 - 基于名字空間的UUID(SHA1版):將版本3的散列算法改為SHA1

3.UUID各版本優(yōu)缺點(diǎn)

  • 版本1 - 基于時(shí)間的UUID:
    • 優(yōu)點(diǎn):能基本保證全球唯一性
    • 缺點(diǎn):使用了Mac地址,因此會(huì)暴露Mac地址和生成時(shí)間
  • 版本2 - 分布式安全的UUID:
    • 優(yōu)點(diǎn):能保證全球唯一性
    • 缺點(diǎn):很少使用,常用庫基本沒有實(shí)現(xiàn)
  • 版本3 - 基于名字空間的UUID(MD5版):
    • 優(yōu)點(diǎn):不同名字空間或名字下的UUID是唯一的;相同名字空間及名字下得到的UUID保持重復(fù)。
    • 缺點(diǎn):MD5碰撞問題,只用于向后兼容,后續(xù)不再使用
  • 版本4 - 基于隨機(jī)數(shù)的UUID:
    • 優(yōu)點(diǎn):實(shí)現(xiàn)簡單
    • 缺點(diǎn):重復(fù)幾率可計(jì)算
  • 版本5 - 基于名字空間的UUID(SHA1版):
    • 優(yōu)點(diǎn):不同名字空間或名字下的UUID是唯一的;相同名字空間及名字下得到的UUID保持重復(fù)。
    • 缺點(diǎn):SHA1計(jì)算相對耗時(shí)

總得來說:
版本 1/2 適用于需要高度唯一性且無需重復(fù)的場景;
版本 3/5 適用于一定范圍內(nèi)唯一且需要或可能會(huì)重復(fù)生成UUID的環(huán)境下;
版本 4 適用于對唯一性要求不太嚴(yán)格且追求簡單的場景。

4.UUID結(jié)構(gòu)及生成規(guī)則

以版本1 - 基于時(shí)間的UUID為例先梳理UUID的結(jié)構(gòu):

UUID為32位的十六機(jī)制數(shù),因此實(shí)際上是16-byte(128-bit),各位分別為:

位置 內(nèi)容 說明
15 – 12 TimeLow 時(shí)間值的低位 4-byte
11 – 10 TimeMid 時(shí)間值的中位 2-byte
09 VersionAndTimeHigh 版本號(hào) 4-bit + 時(shí)間值高位 4-bit
08 TimeHigh 時(shí)間值的高位 1-byte
07 VariantAndClockSeqHigh 變體值 2-bit + 時(shí)鐘序列高位 6-bit
06 ClockSeqLow 時(shí)鐘序列低位 1-byte
05 – 00 Node 節(jié)點(diǎn)值 6-byte

時(shí)間值:在基于時(shí)間的UUID中,時(shí)間值是一個(gè)60位的整型值,對應(yīng)UTC的100ns時(shí)間間隔計(jì)數(shù),因此其支持支持一臺(tái)機(jī)器每秒生成10M次。在UUID中,將這60位放置到了15~08這8-byte中(除了09位有4-bit的版本號(hào)內(nèi)容)。

版本號(hào):版本號(hào)即上文所說的五個(gè)版本,在五個(gè)版本的UUID中,都總是在該位置標(biāo)識(shí)版本,占據(jù) 4-bit,分別以下列數(shù)字表示:

7 6 5 4 版本 說明
0 0 0 1 1 基于時(shí)間的UUID
0 0 1 0 2 分布式安全的UUID
0 0 1 1 3 基于名字空間的UUID(MD5版)
0 1 0 0 4 基于隨機(jī)數(shù)的UUID
0 1 0 1 5 基于名字空間的UUID(SHA1版)

因此版本號(hào)這一位的取值只會(huì)是1,2,3,4,5

變體值:表明所依賴的標(biāo)準(zhǔn)(X表示可以是任意值):

7 6 5 說明
0 X X NCS向后兼容
1 0 X 本標(biāo)準(zhǔn)
1 1 0 Microsoft向后兼容
1 1 1 ITU-T Rec. X.667保留

時(shí)鐘序列:在基于時(shí)間的UUID中,時(shí)鐘序列占據(jù)了07~06位的14-bit。不同于時(shí)間值,時(shí)鐘序列實(shí)際上是表示一種邏輯序列,用于標(biāo)識(shí)事件發(fā)生的順序。在此,如果前一時(shí)鐘序列已知,則可以通過自增來實(shí)現(xiàn)時(shí)鐘序列值的改變;否則,通過(偽)隨機(jī)數(shù)來設(shè)置。主要用于避免因時(shí)間值向未來設(shè)置或節(jié)點(diǎn)值改變可能導(dǎo)致的UUID重復(fù)問題。

節(jié)點(diǎn)值:在基于時(shí)間的UUID中,節(jié)點(diǎn)值占據(jù)了05~00的48-bit,由機(jī)器的MAC地址構(gòu)成。如果機(jī)器有多個(gè)MAC地址,則隨機(jī)選其中一個(gè);如果機(jī)器沒有MAC地址,則采用(偽)隨機(jī)數(shù)。

了解了基于時(shí)間的UUID結(jié)構(gòu)及生成規(guī)則后,再看看其他版本的UUID生成規(guī)則:

  • 版本2 - 分布式安全的UUID:
    將基于時(shí)間的UUID中時(shí)間戳前四位換為POSIX的UID或GID,其余保持一致。
  • 版本3/5 - 基于名字空間的UUID(MD5/SHA1):
    1. 將命名空間(如DNS、URL、OID等)及名字轉(zhuǎn)換為字節(jié)序列;
    2. 通過MD5/SHA1散列算法將上述字節(jié)序列轉(zhuǎn)換為16字節(jié)哈希值(MD5散列不再推薦,SHA1散列的20位只使用其15~00位);
    3. 將哈希值的 3~0 字節(jié)置于UUID的15~12位;
    4. 將哈希值的 5~4 字節(jié)置于UUID的11~10位;
    5. 將哈希值的 7~6 字節(jié)置于UUID的09~08位,并用相應(yīng)版本號(hào)覆蓋第9位的高4位(同版本1位置);
    6. 將哈希值的 8 字節(jié)置于UUID的07位,并用相應(yīng)變體值覆蓋其高2位(同版本1位置);
    7. 將哈希值的 9 字節(jié)置于UUID的06位(原時(shí)鐘序列位置);
    8. 將哈希值的 15~10 字節(jié)置于UUID的05~00位(原節(jié)點(diǎn)值位置)。
  • 版本4 - 基于隨機(jī)數(shù)的UUID:
    1. 生成16byte隨機(jī)值填充UUID。重復(fù)機(jī)率與隨機(jī)數(shù)產(chǎn)生器的質(zhì)量有關(guān)。若要避免重復(fù)率提高,必須要使用基于密碼學(xué)上的假隨機(jī)數(shù)產(chǎn)生器來生成值才行;
    2. 將變體值及版本號(hào)填到相應(yīng)位置。

5.多版本偽碼

// 版本 1 - 基于時(shí)間的UUID:
gen_uuid() {
    struct uuid uu;

    // 獲取時(shí)間戳
    get_time(&clock_mid, &uu.time_low);
    uu.time_mid = (uint16_t) clock_mid; // 時(shí)間中間位
    uu.time_hi_and_version = ((clock_mid >> 16) & 0x0FFF) | 0x1000; // 時(shí)間高位 & 版本號(hào)

    // 獲取時(shí)鐘序列。在libuuid中,嘗試取時(shí)鐘序列+1,取不到則隨機(jī);在python中直接使用隨機(jī)
    get_clock(&uu.clock_seq);// 時(shí)鐘序列+1 或 隨機(jī)數(shù)
    uu.clock_seq |= 0x8000;// 時(shí)鐘序列位 & 變體值

    // 節(jié)點(diǎn)值
    char node_id[6];
    get_node_id(node_id);// 根據(jù)mac地址等獲取節(jié)點(diǎn)id
    uu.node = node_id;
    return uu;
}

// 版本4 - 基于隨機(jī)數(shù)的UUID:
gen_uuid() {
    struct uuid uu;
    uuid_t buf;

    random_get_bytes(buf, sizeof(buf));// 獲取隨機(jī)出來的uuid,如libuuid根據(jù)進(jìn)程id、當(dāng)日時(shí)間戳等進(jìn)行srand隨機(jī)

    uu.clock_seq = (uu.clock_seq & 0x3FFF) | 0x8000;// 變體值覆蓋
    uu.time_hi_and_version = (uu.time_hi_and_version & 0x0FFF) | 0x4000;// 版本號(hào)覆蓋
    return uu;
}

// 版本5 - 基于名字空間的UUID(SHA1版):
gen_uuid(name) {
    struct uuid uu;
    uuid_t buf;

    sha_get_bytes(name, buf, sizeof(buf));// 獲取name的sha1散列出來的uuid

    uu.clock_seq = (uu.clock_seq & 0x3FFF) | 0x8000;// 變體值覆蓋
    uu.time_hi_and_version = (uu.time_hi_and_version & 0x0FFF) | 0x5000;// 版本號(hào)覆蓋
    return uu;
}


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

數(shù)據(jù)庫自增ID可能是大家最熟悉的一種唯一ID生成方式,其具有使用簡單,滿足基本需求,天然有序的優(yōu)點(diǎn),但也有缺陷:

  • 并發(fā)性不好
  • 數(shù)據(jù)庫寫壓力大
  • 數(shù)據(jù)庫故障后不可使用
  • 存在數(shù)量泄露風(fēng)險(xiǎn)

因此這里給出兩種優(yōu)化方案。

1.數(shù)據(jù)庫水平拆分,設(shè)置不同的初始值和相同的步長

如圖所示,可保證每臺(tái)數(shù)據(jù)庫生成的ID是不沖突的,但這種固定步長的方式也會(huì)帶來擴(kuò)容的問題,很容易想到當(dāng)擴(kuò)容時(shí)會(huì)出現(xiàn)無ID初始值可分的窘境,解決方案有:

  • 根據(jù)擴(kuò)容考慮決定步長
  • 增加其他位標(biāo)記區(qū)分?jǐn)U容

這其實(shí)都是在需求與方案間的權(quán)衡,根據(jù)需求來選擇最適合的方式。

2.批量生成一批ID

如果要使用單臺(tái)機(jī)器做ID生成,避免固定步長帶來的擴(kuò)容問題,可以每次批量生成一批ID給不同的機(jī)器去慢慢消費(fèi),這樣數(shù)據(jù)庫的壓力也會(huì)減小到N分之一,且故障后可堅(jiān)持一段時(shí)間。

如圖所示,但這種做法的缺點(diǎn)是服務(wù)器重啟、單點(diǎn)故障會(huì)造成ID不連續(xù)。還是那句話,沒有最好的方案,只有最適合的方案。


雪花算法

定義一個(gè)64bit的數(shù),對指定機(jī)器 & 同一時(shí)刻 & 某一并發(fā)序列,是唯一的,其極限QPS約為400w/s。其格式為:

1 bit 41 bit 10 bit 12 bit
符號(hào)位,不用 時(shí)間戳(最長69年) 機(jī)器id 序列號(hào)

將64 bit分為了四部分。其中時(shí)間戳有時(shí)間上限(69年)。機(jī)器id只有10位,能記錄1024臺(tái)機(jī)器,常用前幾位表示數(shù)據(jù)中心id,后幾位表示數(shù)據(jù)中心內(nèi)的機(jī)器id。序列號(hào)用來對同一個(gè)毫秒之內(nèi)的操作產(chǎn)生不同的ID,最多4095個(gè)。

這種結(jié)構(gòu)是雪花算法提出者Twitter的分法,但實(shí)際上這種算法使用可以很靈活,根據(jù)自身業(yè)務(wù)的并發(fā)情況、機(jī)器分布、使用年限等,可以自由地重新決定各部分的位數(shù),從而增加或減少某部分的量級(jí)。比如百度的UidGenerator、美團(tuán)的Leaf等,都是基于雪花算法做一些適合自身業(yè)務(wù)的變化。

由于雪花算法是強(qiáng)依賴于時(shí)間的,在分布式環(huán)境下,如果發(fā)生時(shí)鐘回?fù)?/strong>,很可能會(huì)引起id沖突的問題。解決方案有:

  • 將ID生成交給少量服務(wù)器,并關(guān)閉時(shí)鐘同步。
  • 直接報(bào)錯(cuò),交給上層業(yè)務(wù)處理。
  • 如果回?fù)軙r(shí)間較短,在耗時(shí)要求內(nèi),比如5ms,那么等待回?fù)軙r(shí)長后再進(jìn)行生成。
  • 如果回?fù)軙r(shí)間很長,那么無法等待,可以勻出少量位(1~2位)作為回?fù)芪?,一旦時(shí)鐘回?fù)?,將回?fù)芪患?,可得到不一樣的ID,2位回?fù)芪辉试S標(biāo)記三次時(shí)鐘回?fù)?,基本夠使用。如果超出了,可以再選擇拋出異常。

方案對比

可以發(fā)現(xiàn),常用的分布式唯一ID生成思路基本是利用一個(gè)長串?dāng)?shù)字或字符串,將其分割成多個(gè)部分,分別記錄時(shí)間信息、機(jī)器/名字信息、隨機(jī)信息、序列信息等。時(shí)間信息部分決定了該策略能使用的時(shí)長,機(jī)器/名字信息支持了分布式環(huán)境下的獨(dú)自生成唯一ID與識(shí)別能力,序列信息保證了事件的順序記錄以及同一時(shí)間單位下的并發(fā)數(shù),而隨機(jī)信息則加大了ID整體的不可識(shí)別性。

實(shí)際上如果現(xiàn)有的方法依然不能滿足,我們完全可以依據(jù)自身業(yè)務(wù)和發(fā)展需求,來自行決定使用何種策略生成唯一ID。各種方案都有其優(yōu)缺點(diǎn),技術(shù)的使用沒有絕對的好壞之分,主要在于是否適合使用場景:

  • 要求生成全局唯一且不會(huì)重復(fù)ID,不關(guān)心順序 —— 使用基于時(shí)間的UUID
    • 如游戲聊天室中不同用戶的身份ID
  • 要求生成唯一ID,具有名稱不可變性,可重復(fù)生成 —— 使用基于名稱哈希的UUID
    • 如基于不可變信息生成的用戶ID,若不小心刪除,仍可根據(jù)信息重新生成同一ID
  • 要求生成有序且自然增長的ID —— 使用數(shù)據(jù)庫自增ID
    • 如各業(yè)務(wù)操作流水ID,高并發(fā)下可參考優(yōu)化方案
  • 要求生成數(shù)值型無序定長ID —— 使用雪花算法
    • 如對存儲(chǔ)空間、查詢效率、傳輸數(shù)據(jù)量等有較高要求的場景

對于最初我們定義的唯一ID特性,各方案的對比如下:

方案 唯一性 有序性 可用性 自主性 安全性
基于時(shí)間的UUID 強(qiáng)唯一性 時(shí)間序+邏輯序 高并發(fā)可用 自主生成 暴露時(shí)間及MAC
基于隨機(jī)值的UUID 依賴隨機(jī)算法 無序 依賴隨機(jī)算法 自主生成 安全
基于名字哈希的UUID 強(qiáng)唯一性 無序 高可用 自主生成 較安全
數(shù)據(jù)庫自增ID 強(qiáng)唯一性 有序 較高可用 依賴中心主機(jī) 暴露數(shù)量
數(shù)據(jù)庫批量ID 強(qiáng)唯一性 批量內(nèi)有序 較高可用 依賴中心主機(jī) 暴露數(shù)量
雪花算法 較強(qiáng)唯一性 時(shí)間序+邏輯序 高并發(fā)可用 自主生成 暴露時(shí)間

從沖突率、QPS和算法時(shí)間復(fù)雜度來比較的話:

方案 沖突率/最高不沖突QPS 時(shí)間復(fù)雜度
基于時(shí)間的UUID 10M/s 下不沖突 時(shí)間戳與時(shí)鐘序列的獲取為固定時(shí)間
基于隨機(jī)值的UUID 依賴隨機(jī)算法 依賴隨機(jī)數(shù)生成算法
基于名字哈希的UUID SHA1有 1 / 10 ^ 48 的機(jī)率沖突 SHA1算法時(shí)間復(fù)雜度為固定時(shí)間
數(shù)據(jù)庫自增ID 不沖突 InnoDB直接增加內(nèi)存中計(jì)數(shù)器的值
數(shù)據(jù)庫批量ID 不沖突 O(n),n為批量值大小
雪花算法 400W/s(取決于序列號(hào)位數(shù)) 各部分?jǐn)?shù)值的獲取為固定時(shí)間

參考

UUID算法分析
關(guān)于UUID的二三事
UUID百度百科
UUID唯一資源命名空間的來龍去脈
UUID是如何保證唯一性的?
如果再有人問你分布式 ID,這篇文章丟給他
分布式唯一ID的幾種生成方案
UidGenerator-百度
Leaf——美團(tuán)點(diǎn)評(píng)分布式ID生成系統(tǒng)
分布式系統(tǒng):Lamport 邏輯時(shí)鐘


關(guān)注我的公眾號(hào)【月亮與二進(jìn)制】,鵝廠程序員的敲碼間隙,也能讀書觀影練劍寫字,分享給你我的世界

我的博客即將同步至騰訊云+社區(qū),邀請大家一同入駐:https://cloud.tencent.com/developer/support-plan?invite_code=390shptnnmo08

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容