• 基于.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


  • 相关阅读:
    adb命令
    Appium -选择、操作元素
    Appium -选择、操作元素2
    Appium环境安装
    selenium报错
    TCP和UDP的区别
    charles抓包的安装,使用说明以及常见问题解决(windows)
    网关协议学习:CGI、FastCGI、WSGI
    理论经典:TCP协议的3次握手与4次挥手过程详解
    OSI七层与TCP/IP五层网络架构详解
  • 原文地址:https://www.cnblogs.com/leafly/p/10135431.html
Copyright © 2020-2023  润新知