概述
前一篇文章講述了最流行的分布式ID生成算法snowflake,本篇文章根據美團點評分布式ID生成系統文章,介紹另一種相對更容易理解和編寫的分布式ID生成方式。
實現原理
Leaf這個名字是來自德國哲學家、數學家萊布尼茨的一句話:
There are no two identical leaves in the world
"世界上沒有兩片相同的樹葉"
設置數據表主鍵自增是最簡單的方案,缺點也很明顯:
強依賴數據庫,無法提供高可用
ID生成強依賴單台服務,無法橫向擴展
很容易想到,如果我的應用每次申請一批id,插入數據時順序取一個使用,即將耗盡時再去獲取一批新的id,如此即可在一定程度上減弱與數據庫的關系,同時將單台擴展延伸為獲取id的步長。
負責發放ID的服務既可以使用MySQL服務,也可以使用Redis等服務。
基於MySQL實現
首先我們建立一張數據庫表
DROP TABLE IF EXISTS `leafsegment`; CREATE TABLE `leafsegment` ( `biz_tag` varchar(255) NULL DEFAULT NULL, `max_id` bigint(20) NULL DEFAULT 0, `step` int(11) NULL DEFAULT 5000, `desc` varchar(255) NULL DEFAULT NULL, `update_time` datetime(0) NULL DEFAULT now() ); -- 添加一條初始化數據 INSERT INTO `leafsegment` VALUES ('test', 0, 5000, '測試', '2018-12-06 23:32:11');
數據庫表如下圖
biz_tag:業務標記,不同業務使用不同的值,可以最大限度地利用ID
max_id:當前已經被申請走的最大Id
step:每次申請Id的步長
desc:業務內容描述
update_time:最新一次申請時間
應用如何獲取一批有效ID呢?
Begin UPDATE leafsegment SET max_id=max_id+step,update_time=now() WHERE biz_tag='test' SELECT biz_tag, max_id, step FROM leafsegment WHERE biz_tag='test' Commit
在一個事務周期內完成max_id的更新,和最新數據的獲取,天然解決了資源競爭問題。
而后,我們就可以在應用中將[max_id-step+1,max_id]閉區間的所有值作為ID來使用了。
基於Redis實現
Redis的實現更為簡單,基本原理是利用了Redis的IncrBy命令實現原子加N,具體實現流程無須贅述。
代碼實現
首先我們定義一個傳遞Step(步長)和MaxId(最大值)的DTO
/// <summary> /// 數據單元 /// </summary> public class DataVal { /// <summary> /// 當前最大Id /// </summary> public long MaxId { get; set; } = 1; /// <summary> /// 當前步長 /// </summary> public int Step { get; set; } = 1000; }
這個類僅負責將ID生發器的數據傳入核心類LeafSegment中。核心類的具體實現如下代碼:
/// <summary> /// 美團的Leaf Segment 方案 /// </summary> public class LeafSegment { private long _currentStep = long.MaxValue >> 1; private readonly Func<DataVal> _idGetAction; private readonly ConcurrentQueue<long> _data = new ConcurrentQueue<long>(); private readonly AutoResetEvent _autoReset = new AutoResetEvent(false); /// <summary> /// 美團的Leaf Segment 方案 /// </summary> /// <param name="idGetAction">Id生成策略</param> /// <param name="prefill">是否立即初始化數據</param> public LeafSegment(Func<DataVal> idGetAction,bool prefill=false) { _idGetAction = idGetAction; if (prefill) { FillData(); } Loop(); } /// <summary> /// 獲取下一個Id /// </summary> /// <returns></returns> public long NextId() { _autoReset.Set(); if (_data.TryDequeue(out var result)) { return result; } throw new Exception("Resource not enough"); } private void Loop() { (new Thread(_ => { while (true) { _autoReset.WaitOne(); FillData(); } }) {IsBackground = true}).Start(); } private void FillData() { //數量小於步長一半時觸發拉新 while (_data.Count < (_currentStep >> 1)) { var tmp = _idGetAction.Invoke(); _currentStep = tmp.Step; for (var i = tmp.MaxId - tmp.Step + 1; i <= tmp.MaxId; i++) { _data.Enqueue(i); } } } }
此處需要注意的是LeafSegment構造函數的第一個入參IdGetAction是一個返回DataVal的回調函數,因此外部實現中可以在該回調函數中返回所需ID序列;
第二個參數prefill,該參數控制實例化LeafSegment對象時,是否同步調用獲取ID區段,如該值為false,將會由啟動的線程稍后補充數據。
完整實現、使用Demo以及benchmark測試請參見源代碼:https://github.com/sampsonye/nice