分布式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

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM