概述
前一篇文章讲述了最流行的分布式ID生成算法snowflake,本篇文章根据美团点评分布式ID生成系统文章,介绍另一种相对更容易理解和编写的分布式ID生成方式。
实现原理
Leaf这个名字是来自德国哲学家、数学家莱布尼茨的一句话:
There are no two identical leaves in the world
"世界上没有两片相同的树叶"
设置数据表主键自增是最简单的方案,缺点也很明显:
- 强依赖数据库,无法提供高可用 
- ID生成强依赖单台服务,无法横向扩展 
很容易想到,如果我的应用每次申请一批id,插入数据时顺序取一个使用,即将耗尽时再去获取一批新的id,如此即可在一定程度上减弱与数据库的关系,同时将单台扩展延伸为获取id的步长。
负责发放ID的服务既可以使用MySQL服务,也可以使用Redis等服务。

基于MySQL实现
首先我们建立一张数据库表
| DROPTABLEIF EXISTS `leafsegment`;CREATETABLE`leafsegment`  (  `biz_tag` varchar(255) NULLDEFAULTNULL,  `max_id` bigint(20) NULLDEFAULT0,  `step` int(11) NULLDEFAULT5000,  `desc` varchar(255)  NULLDEFAULTNULL,  `update_time` datetime(0) NULLDEFAULTnow());-- 添加一条初始化数据INSERTINTO`leafsegment` VALUES('test', 0, 5000, '测试', '2018-12-06 23:32:11'); | 
数据库表如下图

biz_tag:业务标记,不同业务使用不同的值,可以最大限度地利用ID
max_id:当前已经被申请走的最大Id
step:每次申请Id的步长
desc:业务内容描述
update_time:最新一次申请时间
应用如何获取一批有效ID呢?
| BeginUPDATEleafsegment SETmax_id=max_id+step,update_time=now() WHEREbiz_tag='test'SELECTbiz_tag, max_id, step FROMleafsegment WHEREbiz_tag='test'Commit | 
在一个事务周期内完成max_id的更新,和最新数据的获取,天然解决了资源竞争问题。
而后,我们就可以在应用中将[max_id-step+1,max_id]闭区间的所有值作为ID来使用了。
基于Redis实现
Redis的实现更为简单,基本原理是利用了Redis的IncrBy命令实现原子加N,具体实现流程无须赘述。
代码实现
首先我们定义一个传递Step(步长)和MaxId(最大值)的DTO
|     /// <summary>    /// 数据单元    /// </summary>    publicclassDataVal    {        /// <summary>        /// 当前最大Id        /// </summary>        publiclongMaxId { get; set; } = 1;        /// <summary>        /// 当前步长        /// </summary>        publicintStep { get; set; } = 1000;    } | 
这个类仅负责将ID生发器的数据传入核心类LeafSegment中。核心类的具体实现如下代码:
|     /// <summary>    /// 美团的Leaf Segment 方案    /// </summary>    publicclassLeafSegment    {        privatelong_currentStep = long.MaxValue >> 1;        privatereadonlyFunc<DataVal> _idGetAction;        privatereadonlyConcurrentQueue<long> _data = newConcurrentQueue<long>();        privatereadonlyAutoResetEvent _autoReset = newAutoResetEvent(false);        /// <summary>        /// 美团的Leaf Segment 方案        /// </summary>        /// <param name="idGetAction">Id生成策略</param>        /// <param name="prefill">是否立即初始化数据</param>        publicLeafSegment(Func<DataVal> idGetAction,boolprefill=false)        {            _idGetAction = idGetAction;            if(prefill)            {                FillData();            }            Loop();        }        /// <summary>        /// 获取下一个Id        /// </summary>        /// <returns></returns>        publiclongNextId()        {            _autoReset.Set();            if(_data.TryDequeue(outvarresult))            {                returnresult;            }            thrownewException("Resource not enough");        }        privatevoidLoop()        {            (newThread(_ =>            {                while(true)                {                    _autoReset.WaitOne();                    FillData();                }            }) {IsBackground = true}).Start();        }        privatevoidFillData()        {            //数量小于步长一半时触发拉新            while(_data.Count < (_currentStep >> 1))            {                vartmp = _idGetAction.Invoke();                _currentStep = tmp.Step;                for(vari = 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
原文地址: https://www.cnblogs.com/leafly/p/10135431.html
.NET社区新闻,深度好文,欢迎访问公众号文章汇总 http://www.csharpkit.com 