分佈式ID生成服務,推薦你們搞一個太香了

目錄
  1. 闡述背景
  2. Leaf snowflake 模式介紹
  3. Leaf segment 模式介紹
  4. Leaf 改造支持RPC
闡述背景

不吹噓,不誇張,項目中用到ID生成的場景確實挺多。好比業務要作冪等的時候,若是沒有合適的業務字段去作惟一標識,那就須要單獨生成一個惟一的標識,這個場景相信你們不陌生。html

不少時候爲了圖方即可能就是寫一個簡單的ID生成工具類,直接開用。作的好點的可能單獨出一個Jar包讓其餘項目依賴,作的很差的頗有可能就是Copy了N份同樣的代碼。git

單獨搞一個獨立的ID生成服務很是有必要,固然咱們也不必本身作造輪子,有現成開源的直接用就是了。若是人手夠,不差錢,自研也能夠。github

今天爲你們介紹一款美團開源的ID生成框架Leaf,在Leaf的基礎上稍微擴展下,增長RPC服務的暴露和調用,提升ID獲取的性能。redis

Leaf介紹

Leaf 最先期需求是各個業務線的訂單ID生成需求。在美團早期,有的業務直接經過DB自增的方式生成ID,有的業務經過redis緩存來生成ID,也有的業務直接用UUID這種方式來生成ID。以上的方式各自有各自的問題,所以咱們決定實現一套分佈式ID生成服務來知足需求。算法

目前Leaf覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅遊、貓眼電影等衆多業務線。在4C8G VM基礎上,經過公司RPC方式調用,QPS壓測結果近5w/s,TP999 1ms。spring

snowflake模式

snowflake是Twitter開源的分佈式ID生成算法,被普遍應用於各類生成ID的場景。Leaf中也支持這種方式去生成ID。sql

使用步驟以下:數據庫

修改配置leaf.snowflake.enable=true開啓snowflake模式。apache

修改配置leaf.snowflake.zk.address和leaf.snowflake.port爲你本身的Zookeeper地址和端口。bootstrap

想必你們很好奇,爲何這裏依賴了Zookeeper呢?

那是由於snowflake的ID組成中有10bit的workerId,以下圖:

分佈式ID生成服務,推薦你們搞一個太香了

 

圖片

通常若是服務數量很少的話手動設置也沒問題,還有一些框架中會採用約定基於配置的方式,好比基於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的架構圖:

分佈式ID生成服務,推薦你們搞一個太香了

 

圖片

從上圖咱們能夠看出,當多個服務同時對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便可。

<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());
    }
}
相關文章
相關標籤/搜索