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