在單庫單表時,業務 ID 可以依賴數據庫的自增主鍵實現,現在我們把存儲拆分到了多處,如果還是用數據庫的自增主鍵,就會出現主鍵重複的情況。
所以我們不得不面對的一個選擇,就是ID生成器,使用一個唯一的字符串,來標識一條完整的記錄。
這時候,不能使用md5或者sha1來對整個記錄做摘要,因爲我們後續還要改動這個記錄。也不能使用單機的計數器,因爲計數器容易重啓清零,也會存在多臺機器上的數值重複,這違背了無狀態服務的建設目標。
UUID
雖然UUID在大多數語言中都有相關的類庫,但除非迫不得以,我們一般不會使用它。UUID雖然不會重複,但它非常的長,長的讓人望而生畏。
標準的UUID有5個部分組成:8-4-4-4-12,一共32個十六進制字符。因此,一共是128位。當把UUID作爲數據庫的索引時,會因爲它沒有順序性造成索引的隨機分佈和因爲數據量巨大造成查詢性能降低。
- 且無序會造成每一次UUID數據的插入都會對主鍵的b+樹進行很大的修改, 會產生離散 IO,從而產生性能瓶頸。
同時,UUID也是不可讀的,如果你把它打印在紙質的訂單上,並不是一個好的主意。UUID同時還有信息安全的隱患,它的數據計算裏有MAC地址的參與,比較知名的是,曾被用於尋找梅麗莎病毒的製作者位置。
MySQL8以後
MySQL 8.0 推出了函數 UUID_TO_BIN,它可以把 UUID 字符串:
- 通過參數將時間高位放在最前,解決了 UUID 插入時亂序問題;
- 去掉了無用的字符串"-",精簡存儲空間;
- 將字符串其轉換爲二進制值存儲,空間最終從之前的 36 個字節縮短爲了 16 字節。
同時還提供了 BIN_TO_UUID,支持將二進制值反轉爲 UUID 字符串,不用擔心 UUID 的性能和存儲佔用的空間問題,相關的插入性能測試,結果如下表所示:
由於UUID_TO_BIN轉換爲的結果是16 字節,僅比自增 ID 增加 8 個字節,最後存儲佔用的空間也僅比自增大了 3G。
而且由於 UUID 能保證全局唯一,因此使用 UUID 的收益遠遠大於自增ID。在海量併發的互聯網業務場景下,更推薦 UUID 這樣的全局唯一值做主鍵。
但請牢記:分佈式數據庫架構,僅用 UUID 做主鍵依然是不夠的。
數據庫自增ID
當數據量龐大時,在數據庫分庫分表後,數據庫自增id不能滿足唯一id來標識數據;因爲每個表都按自己節奏自增,會造成id衝突,無法滿足需求
改造時間戳
如果你是單機應用,那麼使用時間戳沒什麼問題,即使不用納秒,使用毫秒也是足夠的。但在分佈式環境下面,時間戳同樣不是一個好的選擇。
即使你在機器安裝了 ntpd 時間同步,但由於網絡和機器的差異,計算機的時鐘總是存在差異,你的時間戳總會出現重複。爲了解決這個問題,你需要增加一些其他的標識,比如機器的ID,或者更多細分的信息減少時間的碰撞。
這種自定義的ID生成器,只適合特定的業務,做着做着你就會發現,它本質上是雪花算法的變種。
全局ID生成器服務
可以設計一個全局 ID 生成器服務,每次找服務索要主鍵,這樣雖然可以在業務間實現全局唯一,但是完全依賴全局 ID 生成服務,依賴性大,服務一旦宕機,會影響所有相關依賴服務。
例如使用Redis的計數器,原子性自增,好處在於使用內存,併發性能好,但存在數據丟失;自增數據量泄露的問題
雪花算法
Twitter 雪花算法生成後是一個 64bit 的 long 型的數值,默認字符串長度是19位,它分爲4個部分,基本保持了自增
包含四個組成部分
不使用:1bit,最高位是符號位,0 表示正,1 表示負,固定爲 0
時間戳:41bit,毫秒級的時間戳(41 位的長度可以使用 69 年)
標識位:5bit 數據中心 ID,5bit 工作機器 ID,兩個標識位組合起來最多可以支持部署 1024 個節點(2^10 = 1024 個節點)
如果是分佈式應用部署應保證每個工作進程的標識位id是不同的
序列號:12bit 遞增序列號,表示節點毫秒內生成重複,通過序列號表示唯一,12bit 每毫秒可產生 4096 個 ID
通過序列號 1 毫秒可以產生 4096 個不重複 ID,則 1 秒可以生成 4096 * 1000 = 409w ID
默認的雪花算法是 64 bit,具體的長度可以自行配置。如果希望運行更久,增加時間戳的位數;如果需要支持更多節點部署,增加標識位長度;如果併發很高,增加序列號位數
總結:雪花算法並不是一成不變的,可以根據系統內具體場景進行定製
SnowFlake 算法的優點:
- 高性能高可用:生成時不依賴於數據庫,完全在內存中生成
- 高吞吐:每秒鐘能生成數百萬的自增 ID
- ID 自增:存入數據庫中,索引效率高
SnowFlake 算法的缺點: 依賴與系統時間的一致性,如果系統時間被回調,或者改變,可能會造成 ID 衝突或者重複
適用場景
因爲雪花算法有序自增,保障了 MySQL 中 B+ Tree 索引結構插入高性能
所以,日常業務使用中,雪花算法更多是被應用在數據庫的主鍵 ID 和業務關聯主鍵
存在的問題
機器標識位一致
標識位重複的情況下,雪花 ID 也可能會重複,比如:
- 服務通過集羣的方式部署,其中部分機器標識位一致
時鐘回撥的問題
爲什麼會有時鐘回撥問題
- 有人篡改了宿主機的系統時間
- 集羣中可能會進行整體的時鐘同步,從而修改機器的本地時間
時鐘回撥對雪花算法的影響
如果篡改了本地時間,那就有風險產生重複的ID,而且無法滿足趨勢遞增了。
解決思路
- 方案一:想辦法探測到時鐘回撥,然後做出對應的策略
- 方案二:探索一種ID生成的方式,不完全依靠時間戳來保證雪花算法,或者直接使用別的策略替代時間戳
JS的坑
值得注意的是,雪花算法在JavaScript中有一個坑。後端在返回ID的時候,需要使用String類型代替Long類型,否則會產生預想不到的錯誤。
這是因爲。在JavaScript中,存在兩種數字。Number和BigInt。最常用的,就是number。
最大的Number,叫做Number.MAX_SAFE_INTEGER
,它的值爲:
- 2^53-1 或者
- +/- 9,007,199,254,740,991
衆所周知,Java中的Long,是64位的。Js中的這個安全Integer,完全達不到Java中定義的長度。
這就是萬惡的IEEE_754
規範,它在Long長度大於17位時會出現精度丟失的問題。
常見實現方案
百度(uid-generator)
uid-generator
是由百度技術部開發,項目地址:uid-generator
uid-generator
是基於Snowflake
算法實現的,與原始的snowflake
算法不同在於,uid-generator
支持自定義時間戳、工作機器ID和序列號等各部分的位數,而且uid-generator
中採用用戶自定義workId
的生成策略。
uid-generator
需要與數據庫配合使用,需要新增一個WORKER_NODE
表。 當應用啓動時會向數據庫表中去插入一條數據,插入成功後返回的自增ID就是該機器的workId
數據由host
,port
組成。
美團(Leaf)
github地址:Leaf
美團的Leaf
也是一個分佈式ID生成框架。它非常全面,即支持號段模式,也支持snowflake
模式。
號段模式:依賴於數據庫,但是區別於數據庫主鍵自增的模式。假設100爲一個號段100,200,300,每取一次可以獲得100個ID,性能顯著提高。