1. 程式人生 > >分散式ID生成服務,真的有必要搞一個

分散式ID生成服務,真的有必要搞一個

## 目錄 1. 闡述背景 2. Leaf snowflake 模式介紹 3. Leaf segment 模式介紹 4. Leaf 改造支援RPC ## 闡述背景 不吹噓,不誇張,專案中用到ID生成的場景確實挺多。比如業務要做冪等的時候,如果沒有合適的業務欄位去做唯一標識,那就需要單獨生成一個唯一的標識,這個場景相信大家不陌生。 很多時候為了塗方便可能就是寫一個簡單的ID生成工具類,直接開用。做的好點的可能單獨出一個Jar包讓其他專案依賴,做的不好的很有可能就是Copy了N份一樣的程式碼。 單獨搞一個獨立的ID生成服務非常有必要,當然我們也沒必要自己做造輪子,有現成開源的直接用就是了。如果人手夠,不差錢,自研也可以。 今天為大家介紹一款美團開源的ID生成框架Leaf,在Leaf的基礎上稍微擴充套件下,增加RPC服務的暴露和呼叫,提高ID獲取的效能。 ## Leaf介紹 Leaf 最早期需求是各個業務線的訂單ID生成需求。在美團早期,有的業務直接通過DB自增的方式生成ID,有的業務通過redis快取來生成ID,也有的業務直接用UUID這種方式來生成ID。以上的方式各自有各自的問題,因此我們決定實現一套分散式ID生成服務來滿足需求。 具體Leaf 設計文件見:[https://tech.meituan.com/2017/04/21/mt-leaf.html](https://tech.meituan.com/2017/04/21/mt-leaf.html) 目前Leaf覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅遊、貓眼電影等眾多業務線。在4C8G VM基礎上,通過公司RPC方式呼叫,QPS壓測結果近5w/s,TP999 1ms。 ### snowflake模式 snowflake是Twitter開源的分散式ID生成演算法,被廣泛應用於各種生成ID的場景。Leaf中也支援這種方式去生成ID。 使用步驟如下: 修改配置leaf.snowflake.enable=true開啟snowflake模式。 修改配置leaf.snowflake.zk.address和leaf.snowflake.port為你自己的Zookeeper地址和埠。 想必大家很好奇,為什麼這裡依賴了Zookeeper呢? 那是因為snowflake的ID組成中有10bit的workerId,如下圖: ![](https://img2020.cnblogs.com/blog/1618095/202007/1618095-20200722120043903-210569350.png) 一般如果服務數量不多的話手動設定也沒問題,還有一些框架中會採用約定基於配置的方式,比如基於IP生成wokerID,基於hostname最後幾位生成wokerID,手動在機器上配置,手動在程式啟動時傳入等等方式。 Leaf中為了簡化wokerID的配置,所以採用了Zookeeper來生成wokerID。就是用了Zookeeper持久順序節點的特性自動對snowflake節點配置wokerID。 如果你公司沒有用Zookeeper,又不想因為Leaf去單獨部署Zookeeper的話,你可以將原始碼中這塊的邏輯改掉,比如自己提供一個生成順序ID的服務來替代Zookeeper。 ### segment模式 segment是Leaf基於資料庫實現的ID生成方案,如果呼叫量不大,完全可以用Mysql的自增ID來實現ID的遞增。 Leaf雖然也是基於Mysql,但是做了很多的優化,下面簡單的介紹下segment模式的原理。 首先我們需要在資料庫中新增一張表用於儲存ID相關的資訊。 ```plain CREATE TABLE `leaf_alloc` (   `biz_tag` varchar(128) NOT NULL DEFAULT '',   `max_id` bigint(20) NOT NULL DEFAULT '1',   `step` int(11) NOT NULL,   `description` varchar(256) DEFAULT NULL,   `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,   PRIMARY KEY (`biz_tag`) ) ENGINE=InnoDB; ``` biz_tag用於區分業務型別,比如下單,支付等。如果以後有效能需求需要對資料庫擴容,只需要對biz_tag分庫分表就行。 max_id表示該biz_tag目前所被分配的ID號段的最大值。 step表示每次分配的號段長度。 下圖是segment的架構圖: ![](https://img2020.cnblogs.com/blog/1618095/202007/1618095-20200722120056192-1700992705.png) 從上圖我們可以看出,當多個服務同時對Leaf進行ID獲取時,會傳入對應的biz_tag,biz_tag之間是相互隔離的,互不影響。 比如Leaf有三個節點,當test_tag第一次請求到Leaf1的時候,此時Leaf1的ID範圍就是1~1000。 當test_tag第二次請求到Leaf2的時候,此時Leaf2的ID範圍就是1001~2000。 當test_tag第三次請求到Leaf3的時候,此時Leaf3的ID範圍就是2001~3000。 比如Leaf1已經知道自己的test_tag的ID範圍是1~1000,那麼後續請求過來獲取test_tag對應ID時候,就會從1開始依次遞增,這個過程是在記憶體中進行的,效能高。不用每次獲取ID都去訪問一次資料庫。 #### 問題一 這個時候又有人說了,如果併發量很大的話,1000的號段長度一下就被用完了啊,此時就得去申請下一個範圍,這期間進來的請求也會因為DB號段沒有取回來,導致執行緒阻塞。 放心,Leaf中已經對這種情況做了優化,不會等到ID消耗完了才去重新申請,會在還沒用完之前就去申請下一個範圍段。併發量大的問題你可以直接將step調大即可。 #### 問題二 這個時候又有人說了,如果Leaf服務掛掉某個節點會不會有影響呢? 首先Leaf服務是叢集部署,一般都會註冊到註冊中心讓其他服務發現。掛掉一個沒關係,還有其他的N個服務。問題是對ID的獲取有問題嗎? 會不會出現重複的ID呢? 答案是沒問題的,如果Leaf1掛了的話,它的範圍是1~1000,假如它當前正獲取到了100這個階段,然後服務掛了。服務重啟後,就會去申請下一個範圍段了,不會再使用1~1000。所以不會有重複ID出現。 ## Leaf改造支援RPC 如果你們的呼叫量很大,為了追求更高的效能,可以自己擴充套件一下,將Leaf改造成Rpc協議暴露出去。 首先將Leaf的Spring版本升級到5.1.8.RELEASE,修改父pom.xml即可。 ```plain ``` 然後將Spring Boot的版本升級到2.1.6.RELEASE,修改leaf-server的pom.xml。 ```plain ``` 還需要在leaf-server的pom中增加nacos相關的依賴,因為我們kitty-cloud是用的nacos。同時還需要依賴dubbo,才可以暴露rpc服務。 ```plain ``` 在resource下建立bootstrap.properties檔案,增加nacos相關的配置資訊。 ```plain spring.application.name=LeafSnowflake dubbo.scan.base-packages=com.sankuai.inf.leaf.server.controller dubbo.protocol.name=dubbo dubbo.protocol.port=20086 dubbo.registry.address=spring-cloud://localhost spring.cloud.nacos.discovery.server-addr=47.105.66.210:8848 spring.cloud.nacos.config.server-addr=${spring.cloud.nacos.discovery.server-addr} ``` Leaf預設暴露的Rest服務是LeafController中,現在的需求是既要暴露Rest又要暴露RPC服務,所以我們抽出兩個介面。一個是Segment模式,一個是Snowflake模式。 Segment模式呼叫客戶端 ```plain /** * 分散式ID服務客戶端-Segment模式 * * @作者 尹吉歡 * @個人微信 jihuan900 * @微信公眾號 猿天地 * @GitHub https://github.com/yinjihuan * @作者介紹 http://cxytiandi.com/about * @時間 2020-04-06 16:20 */ @FeignClient("${kitty.id.segment.name:LeafSegment}") public interface DistributedIdLeafSegmentRemoteService { @RequestMapping(value = "/api/segment/get/{key}") String getSegmentId(@PathVariable("key") String key); } ``` Snowflake模式呼叫客戶端 ```plain /** * 分散式ID服務客戶端-Snowflake模式 * * @作者 尹吉歡 * @個人微信 jihuan900 * @微信公眾號 猿天地 * @GitHub https://github.com/yinjihuan * @作者介紹 http://cxytiandi.com/about * @時間 2020-04-06 16:20 */ @FeignClient("${kitty.id.snowflake.name:LeafSnowflake}") public interface DistributedIdLeafSnowflakeRemoteService { @RequestMapping(value = "/api/snowflake/get/{key}") String getSnowflakeId(@PathVariable("key") String key); } ``` 使用方可以根據使用場景來決定用RPC還是Http進行呼叫,如果用RPC就@Reference注入Client,如果要用Http就用@Autowired注入Client。 最後改造LeafController同時暴露兩種協議即可。 ```plain @Service(version = "1.0.0", group = "default") @RestController public class LeafController implements DistributedIdLeafSnowflakeRemoteService, DistributedIdLeafSegmentRemoteService { private Logger logger = LoggerFactory.getLogger(LeafController.class); @Autowired private SegmentService segmentService; @Autowired private SnowflakeService snowflakeService; @Override public String getSegmentId(@PathVariable("key") String key) { return get(key, segmentService.getId(key)); } @Override 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()); } } ``` 擴充套件後的原始碼參考:[https://github.com/yinjihuan/Leaf/tree/rpc_support](https://github.com/yinjihuan/Leaf/tree/rpc_support) **感興趣的Star下唄:**[https://github.com/yinjihuan/kitty](https://github.com/yinjihuan/kitty-cloud) ***關於作者***:尹吉歡,簡單的技術愛好者,《Spring Cloud微服務-全棧技術與案例解析》, 《Spring Cloud微服務 入門 實戰與進階》作者, 公眾號 ****猿天地**** 發起人。個人微信 ****jihuan900****,歡迎勾搭。