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

猿天地發表於2020-07-22

目錄

  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

目前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,歡迎勾搭。

相關文章