基於.NET Standard的分布式自增ID算法--美團點評LeafSegment


概述

前一篇文章講述了最流行的分布式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



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM