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