目錄
- 闡述背景
- Leaf snowflake 模式介紹
- Leaf segment 模式介紹
- 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
目前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,如下圖:
一般如果服務數量不多的話手動設定也沒問題,還有一些框架中會採用約定基於配置的方式,比如基於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相關的資訊。
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的架構圖:
從上圖我們可以看出,當多個服務同時對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掛了的話,它的範圍是11000,假如它當前正獲取到了100這個階段,然後服務掛了。服務重啟後,就會去申請下一個範圍段了,不會再使用11000。所以不會有重複ID出現。
Leaf改造支援RPC
如果你們的呼叫量很大,為了追求更高的效能,可以自己擴充套件一下,將Leaf改造成Rpc協議暴露出去。
首先將Leaf的Spring版本升級到5.1.8.RELEASE,修改父pom.xml即可。
<spring.version>5.1.8.RELEASE</spring.version>
然後將Spring Boot的版本升級到2.1.6.RELEASE,修改leaf-server的pom.xml。
<spring-boot-dependencies.version>2.1.6.RELEASE</spring-boot-dependencies.version>
還需要在leaf-server的pom中增加nacos相關的依賴,因為我們kitty-cloud是用的nacos。同時還需要依賴dubbo,才可以暴露rpc服務。
<dependency>
<groupId>com.cxytiandi</groupId>
<artifactId>kitty-spring-cloud-starter-nacos</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>com.cxytiandi</groupId>
<artifactId>kitty-spring-cloud-starter-dubbo</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
</dependency>
在resource下建立bootstrap.properties檔案,增加nacos相關的配置資訊。
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模式呼叫客戶端
/**
* 分散式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模式呼叫客戶端
/**
* 分散式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同時暴露兩種協議即可。
@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
感興趣的Star下唄:https://github.com/yinjihuan/kitty
關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud微服務-全棧技術與案例解析》, 《Spring Cloud微服務 入門 實戰與進階》作者, 公眾號 猿天地 發起人。個人微信 jihuan900,歡迎勾搭。