1. 程式人生 > >不能錯過的分散式ID生成器(Leaf ),好用的一批!

不能錯過的分散式ID生成器(Leaf ),好用的一批!

>本文收錄在個人部落格:[www.chengxy-nds.top](http://www.chengxy-nds.top),技術資料共享,同進步 不瞭解分散式ID的同學,先行去看[《一口氣說出 9種 分散式ID生成方式,面試官有點懵了》](https://mp.weixin.qq.com/s?__biz=MzAxNTM4NzAyNg==&mid=2247483785&idx=1&sn=8b828a8ae1701b810fe3969be536cb14&chksm=9b859174acf21862f0b95e0502a1a441c496a5488f5466b2e147d7bb9de072bde37c4db25d7a&token=772711575&lang=zh_CN#rd)溫習一下基礎知識,這裡就不再贅述了 ## 美團(Leaf) `Leaf`是美團推出的一個分散式ID生成服務,名字取自德國哲學家、數學家萊布尼茨的一句話:“There are no two identical leaves in the world.”(“世界上沒有兩片相同的樹葉”),取個名字都這麼有寓意,美團程式設計師牛掰啊! `Leaf`的優勢:`高可靠`、`低延遲`、`全域性唯一`等特點。 目前主流的分散式ID生成方式,大致都是基於`資料庫號段模式`和`雪花演算法(snowflake)`,而美團(Leaf)剛好同時兼具了這兩種方式,可以根據不同業務場景靈活切換。 接下來結合實戰,詳細的介紹一下`Leaf`的`Leaf-segment號段模式`和`Leaf-snowflake模式` #### 一、 Leaf-segment號段模式 `Leaf-segment`號段模式是對直接用`資料庫自增ID`充當`分散式ID`的一種優化,減少對資料庫的頻率操作。相當於從資料庫批量的獲取自增ID,每次從資料庫取出一個號段範圍,例如 (1,1000] 代表1000個ID,業務服務將號段在本地生成1~1000的自增ID並載入到記憶體.。 大致的流程入下圖所示: ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228104235797.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbnpoaWZ1MQ==,size_16,color_FFFFFF,t_70) 號段耗盡之後再去資料庫獲取新的號段,可以大大的減輕資料庫的壓力。對`max_id`欄位做一次`update`操作,`update max_id= max_id + step`,update成功則說明新號段獲取成功,新的號段範圍是(`max_id ,max_id +step`]。 由於依賴資料庫,我們先設計一下表結構: ```sql CREATE TABLE `leaf_alloc` ( `biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '業務key', `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '當前已經分配了的最大id', `step` int(11) NOT NULL COMMENT '初始步長,也是動態調整的最小步長', `description` varchar(256) DEFAULT NULL COMMENT '業務key的描述', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '資料庫維護的更新時間', PRIMARY KEY (`biz_tag`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; ``` 預先插入一條測試的業務資料 ```sql INSERT INTO `leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`, `update_time`) VALUES ('leaf-segment-test', '0', '10', '測試', '2020-02-28 10:41:03'); ``` - `biz_tag`:針對不同業務需求,用biz_tag欄位來隔離,如果以後需要擴容時,只需對biz_tag分庫分表即可 - `max_id`:當前業務號段的最大值,用於計算下一個號段 - `step`:步長,也就是每次獲取ID的數量 - ` description`:對於業務的描述,沒啥好說的 將Leaf專案下載到本地:`https://github.com/Meituan-Dianping/Leaf` 修改一下專案中的`leaf.properties`檔案,新增資料庫配置 ```javascript leaf.name=com.sankuai.leaf.opensource.test leaf.segment.enable=true leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/xin-master?useUnicode=true&characterEncoding=utf8 leaf.jdbc.username=junkang leaf.jdbc.password=junkang leaf.snowflake.enable=false ``` **注意**:`leaf.snowflake.enable` 與 `leaf.segment.enable` 是無法同時開啟的,否則專案將無法啟動。 配置相當的簡單,直接啟動`LeafServerApplication`後就OK了,接下來測試一下,`leaf`是基於`Http請求`的發號服務, `LeafController` 中只有兩個方法,一個號段介面,一個snowflake介面,`key`就是資料庫中預先插入的業務`biz_tag`。 ```javascript @RestController public class LeafController { private Logger logger = LoggerFactory.getLogger(LeafController.class); @Autowired private SegmentService segmentService; @Autowired private SnowflakeService snowflakeService; /** * 號段模式 * @param key * @return */ @RequestMapping(value = "/api/segment/get/{key}") public String getSegmentId(@PathVariable("key") String key) { return get(key, segmentService.getId(key)); } /** * 雪花演算法模式 * @param key * @return */ @RequestMapping(value = "/api/snowflake/get/{key}") public String getSnowflakeId(@PathVariable("key") String key) { return get(key, snowflakeService.getId(key)); } private String get(@PathVariable("key") String key, Result id) { Result result; if (key == null || key.isEmpty()) { throw new NoKeyException(); } result = id; if (result.getStatus().equals(Status.EXCEPTION)) { throw new LeafServerException(result.toString()); } return String.valueOf(result.getId()); } } ``` 訪問:`http://127.0.0.1:8080/api/segment/get/leaf-segment-test`,結果正常返回,感覺沒毛病,但當查了一下資料庫表中資料時發現了一個問題。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228110053821.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbnpoaWZ1MQ==,size_16,color_FFFFFF,t_70) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228125559915.png) 通常在用號段模式的時候,取號段的時機是在前一個號段消耗完的時候進行的,可剛剛才取了一個ID,資料庫中卻已經更新了`max_id`,也就是說`leaf`已經多獲取了一個號段,這是什麼鬼操作? ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020022812323267.png) **`Leaf`為啥要這麼設計呢?** `Leaf` 希望能在DB中取號段的過程中做到無阻塞! 當號段耗盡時再去DB中取下一個號段,如果此時網路發生抖動,或者DB發生慢查詢,業務系統拿不到號段,就會導致整個系統的響應時間變慢,對流量巨大的業務,這是不可容忍的。 所以`Leaf`在當前號段消費到某個點時,就非同步的把下一個號段載入到記憶體中。而不需要等到號段用盡的時候才去更新號段。這樣做很大程度上的降低了系統的風險。 **那麼`某個點`到底是什麼時候呢?** 這裡做了一個實驗,號段設定長度為`step=10`,`max_id=1`, ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228131230216.png) 當我拿第一個ID時,看到號段增加了,1/10 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228131720306.png) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228131306464.png) 當我拿第三個Id時,看到號段又增加了,3/10 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228131401342.png) ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/2020022813143268.png) `Leaf`採用`雙buffer`的方式,它的服務內部有兩個號段快取區`segment`。當前號段已消耗10%時,還沒能拿到下一個號段,則會另啟一個更新執行緒去更新下一個號段。 簡而言之就是`Leaf`保證了總是會多快取兩個號段,即便哪一時刻資料庫掛了,也會保證發號服務可以正常工作一段時間。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228132506104.png) 通常推薦號段(`segment`)長度設定為服務高峰期發號QPS的600倍(10分鐘),這樣即使DB宕機,Leaf仍能持續發號10-20分鐘不受影響。 **優點:** - Leaf服務可以很方便的線性擴充套件,效能完全能夠支撐大多數業務場景。 - 容災性高:Leaf服務內部有號段快取,即使DB宕機,短時間內Leaf仍能正常對外提供服務。 **缺點:** - ID號碼不夠隨機,能夠洩露發號數量的資訊,不太安全。 - DB宕機會造成整個系統不可用(用到資料庫的都有可能)。 #### 二、Leaf-snowflake `Leaf-snowflake`基本上就是沿用了snowflake的設計,ID組成結構:`正數位`(佔1位元)+ `時間戳`(佔41位元)+ `機器ID`(佔5位元)+ `機房ID`(佔5位元)+ `自增值`(佔12位元),總共64位元組成的一個Long型別。 `Leaf-snowflake`不同於原始snowflake演算法地方,主要是在workId的生成上,`Leaf-snowflake`依靠`Zookeeper`生成`workId`,也就是上邊的`機器ID`(佔5位元)+ `機房ID`(佔5位元)。`Leaf`中workId是基於ZooKeeper的`順序Id`來生成的,每個應用在使用Leaf-snowflake時,啟動時都會都在Zookeeper中生成一個順序Id,相當於一臺機器對應一個順序節點,也就是一個workId。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228135150773.png) `Leaf-snowflake`啟動服務的過程大致如下: - 啟動Leaf-snowflake服務,連線Zookeeper,在leaf_forever父節點下檢查自己是否已經註冊過(是否有該順序子節點)。 - 如果有註冊過直接取回自己的workerID(zk順序節點生成的int型別ID號),啟動服務。 - 如果沒有註冊過,就在該父節點下面建立一個持久順序節點,建立成功後取回順序號當做自己的workerID號,啟動服務。 但`Leaf-snowflake`對Zookeeper是一種弱依賴關係,除了每次會去ZK拿資料以外,也會在本機檔案系統上快取一個`workerID`檔案。一旦ZooKeeper出現問題,恰好機器出現故障需重啟時,依然能夠保證服務正常啟動。 啟動`Leaf-snowflake`模式也比較簡單,起動本地ZooKeeper,修改一下專案中的`leaf.properties`檔案,關閉`leaf.segment模式`,啟用`leaf.snowflake`模式即可。 ```javascript leaf.segment.enable=false #leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/xin-master?useUnicode=true&characterEncoding=utf8 #leaf.jdbc.username=junkang #leaf.jdbc.password=junkang leaf.snowflake.enable=true leaf.snowflake.zk.address=127.0.0.1 leaf.snowflake.port=2181 ``` ```javascript /** * 雪花演算法模式 * @param key * @return */ @RequestMapping(value = "/api/snowflake/get/{key}") public String getSnowflakeId(@PathVariable("key") String key) { return get(key, snowflakeService.getId(key)); } ``` 測試一下,訪問:`http://127.0.0.1:8080/api/snowflake/get/leaf-segment-test` ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228140247768.png) **優點:** - ID號碼是趨勢遞增的8byte的64位數字,滿足上述資料庫儲存的主鍵要求。 **缺點:** - 依賴ZooKeeper,存在服務不可用風險(實在不知道有啥缺點了) ### 三、Leaf監控 請求地址:`http://127.0.0.1:8080/cache` 針對服務自身的監控,Leaf提供了Web層的記憶體資料對映介面,可以實時看到所有號段的下發狀態。比如每個號段雙buffer的使用情況,當前ID下發到了哪個位置等資訊都可以在Web介面上檢視。 ![在這裡插入圖片描述](https://img-blog.csdnimg.cn/20200228145359473.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3hpbnpoaWZ1MQ==,size_16,color_FFFFFF,t_70) ## 總結 對於Leaf具體使用哪種模式,還是根據具體的業務場景使用,本文並沒有對Leaf原始碼做過多的分析,因為Leaf 程式碼量簡潔很好閱讀。 **原創不易,燃燒秀髮輸出內容,如果有一丟丟收穫,點個贊鼓勵一下吧!** 整理了幾百本各類技術電子書,送給小夥伴們。關注公號回覆【666】自行領取。和一些小夥伴們建了一個技術交流群,一起探討技術、分享技術資料,旨在共同學習進步,如果感興趣就加入我們吧! ![](https://imgconvert.csdnimg.cn/aHR0cHM6Ly91c2VyLWdvbGQtY2RuLnhpdHUuaW8vMjAyMC8yLzQvMTcwMGU0Mjk1MDQzMjQ0Yg?x-oss-process=image/form