一、資料庫自增(單例項)
1、方案描述
基於資料庫自增ID(auto_increment)利用其來充當分散式ID。實現方式就是用一張表來充當ID生成器,當我們需要ID時,向表中插入一條記錄返回主鍵ID。
CREATE TABLE identity_table(
`id` INT(11) UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '主鍵id',
`value` VARCHAR(50) NOT NULL DEFAULT '' COMMENT 'value',
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='ID資訊表';
<insert id="insertAndGetId" useGeneratedKeys="true" keyProperty="id">
insert into identity_table(value)
values(#{value})
</insert>
2、優點
(1)通過資料庫保證唯一性,實現簡單;
(2)數字ID,具備有序性。
3、缺點
(1)存在單點宕機風險;
(2)無法抗住高併發場景。
二、資料庫叢集模式
1、方案描述
基於多例項資料庫主鍵自增,通過橫向擴充套件機器,解決單點資料庫的壓力。實現方式就是在自增ID(auto_increment)的基礎上,設定step增長步長,使得不同例項中的ID不會發生碰撞。
set @@auto_increment_offset = 0; -- 起始值
set @@auto_increment_increment = 3; -- 步長

2、優點
(1)利用水平擴充套件機器,解決單點問題;
3、缺點
(1)擴充套件例項,需要修改步長,操作複雜;
(2)產生ID需要依賴資料庫,資料庫的效能依然是瓶頸。
三、Redis
當使用資料庫來生成ID效能不夠要求的時候,我們可以嘗試使用Redis來生成ID。通過Redis的原子操作 INCR和INCRBY來實現。
可以使用Redis叢集來獲取更高的吞吐量。假如一個叢集中有3臺Redis。可以初始化每臺Redis的值分別是0,1,2,然後步長都是3。

優點
(1)不依賴於資料庫,效能優於資料庫;
(2)ID是有序的。
缺點
(1)強依賴於Redis,Redis宕機也會有風險;
(2)有I/O操作,網路抖動會影響服務響應速度。
四、UUID
UUID 是由一組32位數的16進位制數字所構成,是故 UUID 理論上的總數為16^32=2^128,約等於3.4 x 10123。也就是說若每納秒產生1百萬個 UUID,要花100億年才會將所有 UUID 用完。
UUID生成版本:
version 1, date-time & MAC address
version 2, date-time & group/user id
version 3, MD5 hash & namespace
version 4, pseudo-random number
version 5, SHA-1 hash & namespace
Java實現:
/**
Static factory to retrieve a type 4 (pseudo randomly generated) UUID.
The {@code UUID} is generated using a cryptographically strong pseudo
random number generator.
@return A randomly generated {@code UUID}
/
public static UUID randomUUID() {
SecureRandom ng = Holder.numberGenerator;
byte[] randomBytes = new byte[16];
ng.nextBytes(randomBytes);
randomBytes[6] &= 0x0f; / clear version /
randomBytes[6] |= 0x40; / set to version 4 /
randomBytes[8] &= 0x3f; / clear variant /
randomBytes[8] |= 0x80; / set to IETF variant */
return new UUID(randomBytes);
}
碰撞概率:
Java中的UUID使用的是版本4進行實現,128bit中有122bit是隨機產生的,產生的UUID重複概率非常低,所以在使用時可以不考慮此問題。
最終生成UUID:
123e4567-e89b-12d3-a456-426655440000
索引問題:
導致索引重排。
對於B+樹的結構:
(1)孩子數和key的數目相同

優點
(1)實現簡單;
(2)本地生成,無效能瓶頸;
(3)具備唯一性。
缺點
(1)ID是無序的;
(2)ID無特定含義;
(3)UUID是字串且長度較長,儲存與查詢效率慢。
五、號段模式
號段模式可以理解為從資料庫批量的獲取自增ID,每次從資料庫取出一個號段範圍,例如 (1,1000] 代表1000個ID,具體的業務服務將本號段,生成1~1000的自增ID並載入到記憶體。
CREATE TABLE id_generator (
id int(10) NOT NULL,
max_id bigint(20) NOT NULL COMMENT '當前最大id',
step int(20) NOT NULL COMMENT '號段的布長',
biz_type int(20) NOT NULL COMMENT '業務型別',
version int(20) NOT NULL COMMENT '版本號',
PRIMARY KEY (`id`)
)
a. biz_type :代表不同業務型別
b. max_id :當前最大的可用id
c. step :代表號段的長度
d. version :是一個樂觀鎖,每次都更新version,保證併發時資料的正確性

等這批號段ID用完,再次向資料庫申請新號段,對max_id欄位做一次update操作,update max_id= max_id + step,update成功則說明新號段獲取成功,新的號段範圍是(max_id ,max_id +step]。
架構圖:

update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX
由於多業務端可能同時操作,所以採用版本號version樂觀鎖方式更新,這種分散式ID生成方式不強依賴於資料庫,不會頻繁的訪問資料庫,對資料庫的壓力小很多,查詢頻率減小到1/step。
優點
(1)ID號碼是趨勢遞增的,滿足資料庫儲存和查詢效能要求
(2)可用性高,即使ID生成伺服器不可用,也能夠使得業務在短時間內可用。
(3)可以自定義max_id的大小,方便業務遷移,方便機器橫向擴張
缺點
(1)ID號碼不夠隨機,完整的順序遞增可能帶來安全問題;
(2)DB宕機可能導致整個系統不可用,仍然存在這種風險,因為號段只能撐一段時間。
六、雪花演算法(SnowFlake)
SnowFlake演算法是Twitter開源的分散式ID生成演算法。生成長度為64bit的long型的數字作為全域性唯一ID。

a. 1bit
第一個bit是符號位,生成的ID是正數,所以第一個bit是0。
**b. 41bit
**41bit可以表示2^41個毫秒值,相當於69年。
c. 10bit
10bit可以表示工作機器,5bit表示(2^5=32個)機房,剩餘5bit表示(2^5=32臺)機器。表示最多可以標識(2^10=1024臺)機器
d. 12bit
用來標識同一毫秒內的(2^12 - 1 = 4095個)不同的id
優點
(1)高效能高可用:生成時不依賴於資料庫,完全在記憶體中生成;
(2)容量大:每秒中能生成數百萬的自增ID;
(3)ID自增:存入資料庫中,索引效率高。
缺點
(1)依賴系統時鐘,如果時間回撥或改變,會造成ID衝突。
七、Leaf
號段模式優化
資料庫的I/O操作(更新、查詢)會是瓶頸,所以需要對此進行優化。
如果在號段消費完之後才從資料庫獲取新的號段,就會存在2個問題:
(1)查詢DB過程出現網路抖動或慢查詢會導致系統響應時間變長;
(2)DB的I/O操作本身耗時,如果請求進來但號段未取回就會造成阻塞。
雙Buffer
為了能做到DB取號過程能做到無阻塞,當號段消費到某個點時非同步吧下一個號段載入到記憶體中。非同步更新,通過雙Buffer的方式,保證在DB服務出現問題時,仍然能夠有一個Buffer號段能夠使用。
詳細實現如下圖:

採用雙buffer的方式,Leaf服務內部有兩個號段快取區segment。當前號段已下發10%時,如果下一個號段未更新,則另啟一個更新執行緒去更新下一個號段。當前號段全部下發完後,如果下個號段準備好了則切換到下個號段為當前segment接著下發,迴圈往復。
解決時鐘問題
因為這種方案依賴時間,如果機器的時鐘發生了回撥,那麼就會有可能生成重複的ID號,需要解決時鐘回退的問題。


參見上圖整個啟動流程圖
(1)服務啟動時首先檢查自己是否寫過ZooKeeper leaf_forever節點:
(2)若寫過,則用自身系統時間與leaf_forever/{self}時間則認為機器時間發生了大步長回撥,服務啟動失敗並報警;
(3)若未寫過,證明是新服務節點,直接建立持久節點leaf_forever/
(4)若abs( 系統時間-sum(time)/nodeSize ) < 閾值,認為當前系統時間準確,正常啟動服務,同時寫臨時節點leaf_temporary/self並寫入自身系統時間,接下來綜合對比其餘Leaf節點的系統時間來判斷自身系統時間是否準確,具體做法是取leaftemporary下的所有臨時節點(所有執行中的Leaf−snowflake節點)的服務IP:Port,然後通過RPC請求得到所有節點的系統時間,計算sum(time)/nodeSize;(4)若abs(系統時間−sum(time)/nodeSize)<閾值,認為當前系統時間準確,正常啟動服務,同時寫臨時節點leaftemporary/{self} 維持租約;
(5)否則認為本機系統時間發生大步長偏移,啟動失敗並報警;
(6)每隔一段時間(3s)上報自身系統時間寫入leaf_forever/${self};
參考
[1] B+樹簡歷過程(https://www.jianshu.com/p/08fe11a5fbb9)
[2] 美團Leaf原始碼分析(https://www.jianshu.com/p/92d34144dcb7)
[3] 美團點評分散式ID生成系統(https://tech.meituan.com/2017/04/21/mt-leaf.html)
[4] 美團分散式ID生成服務開源(https://tech.meituan.com/2019/03/07/open-source-project-leaf.html)