前言
ID在程序設計中,無處不在,至關重要。
- 分佈式鎖中,我們會用唯一ID宣誓鎖的歸屬。
- 數據庫中用主鍵ID記錄每一行並綁定Data。
- 分庫分表的系統中,用ID生成,來保證全局唯一等等
自己做下總結。
分佈式ID的要求
- UNIQ 唯一性:ID,ID 要的就是唯一
- HP 高性能:生成ID的服務,不能成爲瓶頸
- HA 高可用:保證高可用,如果ID是訂單ID,突然ID服務宕機,影響全局交易就不好了
- 趨勢:遞增還是隨機,看場景需要
知道了基本要求,下面開始介紹各種策略,並分析一下他們的是否達到了這些要求。
1:UUID
uuid例子:f1c09159-97c3-4ac1-8cb1-2d820a8eeb05
經過計算,每秒生成10億個UUID,100年不間斷,有50%的可能產生一個衝突
UUID的碰撞只存在理論上的可能,現實中可以忽略不計。
我們來看下性能:單線程1秒 百萬量級。
因爲是全局唯一,所以我們常常用來做分佈式鎖的唯一標識,保證獲得鎖和釋放鎖的是同一個人。
唯一性:滿足
高性能:滿足
高可用:滿足
趨勢:隨機
2:Redis incr
redis作爲單線程的nosql系統。
使用incr就高性能的生成唯一的單調遞增ID。
唯一性:基本滿足(依賴可用性)
高性能:基本滿足
高可用:不滿足,存於內存,要考慮持久化和容災。集羣模式下重連還要進行主從複製,等待時間比較長。
趨勢:單調遞增
3:Snowflake雪花算法
下面說一說大名鼎鼎的Twitter的雪花算法。
算法結果是一個長整型 Long
大佬們慣用的 位存儲轉基本類型。
基本原理如下圖,一眼就描述清楚了。
- 0位 符號位:0 正數
- 1~41位:毫秒時間戳,減一個固定開始時間,可以延長使用時間
- 42~51位:業務ID
- 52~64位:序列號 自增 2^12 = 4096
每毫秒1024個業務,每個業務能容納4096個ID。
生成效率,30W/s
唯一性:滿足
高性能:滿足
高可用:滿足
趨勢:趨勢遞增
當然這是一種思路
在企業開發中,可以在企業開發中借鑑一下
比如我們生成訂單號就是類似的思路:630558772926921027。
出於保密,我就不具體解讀了。
雪花就沒毛病了嘛?大部分場景其實是的。不過有兩個問題,還是需要考慮:
- 每毫秒4096,還是可能成爲瓶頸的
- 雪花ID是長整型,直接用來做SQL主鍵,也是比較浪費空間的。
- 依賴機器時間,可能會有實現回撥
4: AUTO_INCREMENT數據庫自增
直接使用DB的AUTO_INCREMENT。就能得到遞增的ID。
唯一性:滿足
高性能:IO瓶頸哦
高可用:單機問題
趨勢:單調遞增
純粹個人玩玩可以。線上還要針對性能和可用性進行優化,所以就引入集羣模式。
5:數據庫集羣模式
要數據庫的高性能高可用,肯定要考慮集羣。
有了多態機器,就需要每個機器單獨負責生成號碼。
我們使用 初始值offset 加 步長increment
來看配置
MySQL_1 配置:
set @@auto_increment_offset = 1; -- 起始值
set @@auto_increment_increment = 3; -- 步長
MySQL_2 配置:
set @@auto_increment_offset = 2; -- 起始值
set @@auto_increment_increment = 3; -- 步長
MySQL_3 配置:
set @@auto_increment_offset = 3; -- 起始值
set @@auto_increment_increment = 3; -- 步長
完美解決。
Mysql1生成:1,4,7,10……
Mysql2生成:2,5,8,11……
Mysql3生成:3,6,9,12……
但是發現問題來了。 這裏的步長就等於機器的數量。
未來如果要擴容就非常困難了。。。
唯一性:滿足
高性能:滿足
高可用:集羣高可用
趨勢:趨勢遞增
缺點:
- 無法擴容。定好步長和初始值後,就涼了。再次擴容,一般需要停滯服務,併產生號段空洞。
- 成本高
爲了解決性能瓶頸和處理擴容問題
6:Segment 號段模式
集羣模式,每個ID和數據庫交互一次。
而號段模式,借鑑單次步長step,就是一次SQL交互取出的ID代表了一個號段。
如id=1,step=1000. 代表數字[1,1000].
獲去1個ID的讀寫數據庫的頻率,從1減小到了1/step。
先看例子,我直接使用美團的id生成器Leaf來說明。
先上表。
DB數據
CREATE DATABASE leaf
CREATE TABLE `leaf_alloc` (
`biz_tag` varchar(128) NOT NULL DEFAULT '', -- your biz unique name
`max_id` bigint(20) NOT NULL DEFAULT '1', // 當前被使用的最大ID
`step` int(11) NOT NULL, // 單步步長
`description` varchar(256) DEFAULT NULL,
`update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB;
insert into leaf_alloc(biz_tag, max_id, step, description) values('leaf-segment-test', 1, 2000, 'Test leaf Segment Mode Get Id')
使用leaf-segment-test2 生效ID的效果
可以看到 max_id = 201 而 step=100時。
此時內存中(segment對象)存儲了[101,200] 共100個ID。 進行自增的發號
這樣每100個號碼才IO一次。 瓶頸解決
同時Leaf爲了防止IO獲取號段時,導致的服務不可用,採用提前加載的方式處理。稱爲雙buffer優化
當取到309時
db效果
再取幾個
db效果
可以看到這裏提前開闢了號段。
// 關鍵的判斷: 剩餘 < 總數的90%(發了10%,就會去開闢新號段)。
segment.getIdle() < 0.9 * segment.getStep()
取得的新號段,會在Buffer中存儲,可以看到源碼中,Segment是數組。通過通過維護好currentPos實現提前號段的buffer~ 女少!
唯一性:滿足
高性能:滿足
高可用:集羣高可用
趨勢:趨勢遞增
同時拓展只需新增biz_tag 也比較方便。 我們公司交易的分庫分表就是採用Segment來實現的。
TIPS:第三方框架
美團Leaf:世界上沒有兩片完全相同的樹葉。
https://github.com/Meituan-Dianping/Leaf/blob/feature/spring-boot-starter/README_CN.md
百度:https://github.com/baidu/uid-generator/blob/master/README.zh_cn.md
其實技術選型不是找到最極致的,而是找到最合適的。
所以公司層面,一般都會定製自己的分佈式ID來滿足相關的業務需求。
溜了溜了