不能錯過的分散式ID生成器(Leaf ),好用的一批!

程式設計師內點事發表於2020-08-07

本文收錄在個人部落格:www.chengxy-nds.top,技術資料共享,同進步

不瞭解分散式ID的同學,先行去看《一口氣說出 9種 分散式ID生成方式,面試官有點懵了》溫習一下基礎知識,這裡就不再贅述了

美團(Leaf)

Leaf是美團推出的一個分散式ID生成服務,名字取自德國哲學家、數學家萊布尼茨的一句話:“There are no two identical leaves in the world.”(“世界上沒有兩片相同的樹葉”),取個名字都這麼有寓意,美團程式設計師牛掰啊!

Leaf的優勢:高可靠低延遲全域性唯一等特點。

目前主流的分散式ID生成方式,大致都是基於資料庫號段模式雪花演算法(snowflake),而美團(Leaf)剛好同時兼具了這兩種方式,可以根據不同業務場景靈活切換。

接下來結合實戰,詳細的介紹一下LeafLeaf-segment號段模式Leaf-snowflake模式

一、 Leaf-segment號段模式

Leaf-segment號段模式是對直接用資料庫自增ID充當分散式ID的一種優化,減少對資料庫的頻率操作。相當於從資料庫批量的獲取自增ID,每次從資料庫取出一個號段範圍,例如 (1,1000] 代表1000個ID,業務服務將號段在本地生成1~1000的自增ID並載入到記憶體.。

大致的流程入下圖所示:
在這裡插入圖片描述
號段耗盡之後再去資料庫獲取新的號段,可以大大的減輕資料庫的壓力。對max_id欄位做一次update操作,update max_id= max_id + step,update成功則說明新號段獲取成功,新的號段範圍是(max_id ,max_id +step]。

由於依賴資料庫,我們先設計一下表結構:

CREATE TABLE `leaf_alloc` (
  `biz_tag` varchar(128) NOT NULL DEFAULT '' COMMENT '業務key',
  `max_id` bigint(20) NOT NULL DEFAULT '1' COMMENT '當前已經分配了的最大id',
  `step` int(11) NOT NULL COMMENT '初始步長,也是動態調整的最小步長',
  `description` varchar(256) DEFAULT NULL COMMENT '業務key的描述',
  `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '資料庫維護的更新時間',
  PRIMARY KEY (`biz_tag`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

預先插入一條測試的業務資料

INSERT INTO `leaf_alloc` (`biz_tag`, `max_id`, `step`, `description`, `update_time`) VALUES ('leaf-segment-test', '0', '10', '測試', '2020-02-28 10:41:03');
  • biz_tag:針對不同業務需求,用biz_tag欄位來隔離,如果以後需要擴容時,只需對biz_tag分庫分表即可

  • max_id:當前業務號段的最大值,用於計算下一個號段

  • step:步長,也就是每次獲取ID的數量

  • description:對於業務的描述,沒啥好說的

將Leaf專案下載到本地:https://github.com/Meituan-Dianping/Leaf

修改一下專案中的leaf.properties檔案,新增資料庫配置

leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/xin-master?useUnicode=true&characterEncoding=utf8
leaf.jdbc.username=junkang
leaf.jdbc.password=junkang

leaf.snowflake.enable=false

注意leaf.snowflake.enableleaf.segment.enable 是無法同時開啟的,否則專案將無法啟動。

配置相當的簡單,直接啟動LeafServerApplication後就OK了,接下來測試一下,leaf是基於Http請求的發號服務, LeafController 中只有兩個方法,一個號段介面,一個snowflake介面,key就是資料庫中預先插入的業務biz_tag


@RestController
public class LeafController {
    private Logger logger = LoggerFactory.getLogger(LeafController.class);

    @Autowired
    private SegmentService segmentService;
    @Autowired
    private SnowflakeService snowflakeService;

    /**
     * 號段模式
     * @param key
     * @return
     */
    @RequestMapping(value = "/api/segment/get/{key}")
    public String getSegmentId(@PathVariable("key") String key) {
        return get(key, segmentService.getId(key));
    }

    /**
     * 雪花演算法模式
     * @param key
     * @return
     */
    @RequestMapping(value = "/api/snowflake/get/{key}")
    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());
    }
}

訪問:http://127.0.0.1:8080/api/segment/get/leaf-segment-test,結果正常返回,感覺沒毛病,但當查了一下資料庫表中資料時發現了一個問題。
在這裡插入圖片描述
在這裡插入圖片描述
通常在用號段模式的時候,取號段的時機是在前一個號段消耗完的時候進行的,可剛剛才取了一個ID,資料庫中卻已經更新了max_id,也就是說leaf已經多獲取了一個號段,這是什麼鬼操作?
在這裡插入圖片描述

Leaf為啥要這麼設計呢?

Leaf 希望能在DB中取號段的過程中做到無阻塞!

當號段耗盡時再去DB中取下一個號段,如果此時網路發生抖動,或者DB發生慢查詢,業務系統拿不到號段,就會導致整個系統的響應時間變慢,對流量巨大的業務,這是不可容忍的。

所以Leaf在當前號段消費到某個點時,就非同步的把下一個號段載入到記憶體中。而不需要等到號段用盡的時候才去更新號段。這樣做很大程度上的降低了系統的風險。

那麼某個點到底是什麼時候呢?

這裡做了一個實驗,號段設定長度為step=10max_id=1
在這裡插入圖片描述
當我拿第一個ID時,看到號段增加了,1/10
在這裡插入圖片描述
在這裡插入圖片描述
當我拿第三個Id時,看到號段又增加了,3/10
在這裡插入圖片描述
在這裡插入圖片描述
Leaf採用雙buffer的方式,它的服務內部有兩個號段快取區segment。當前號段已消耗10%時,還沒能拿到下一個號段,則會另啟一個更新執行緒去更新下一個號段。

簡而言之就是Leaf保證了總是會多快取兩個號段,即便哪一時刻資料庫掛了,也會保證發號服務可以正常工作一段時間。

在這裡插入圖片描述
通常推薦號段(segment)長度設定為服務高峰期發號QPS的600倍(10分鐘),這樣即使DB當機,Leaf仍能持續發號10-20分鐘不受影響。

優點:

  • Leaf服務可以很方便的線性擴充套件,效能完全能夠支撐大多數業務場景。
  • 容災性高:Leaf服務內部有號段快取,即使DB當機,短時間內Leaf仍能正常對外提供服務。

缺點:

  • ID號碼不夠隨機,能夠洩露發號數量的資訊,不太安全。
  • DB當機會造成整個系統不可用(用到資料庫的都有可能)。

二、Leaf-snowflake

Leaf-snowflake基本上就是沿用了snowflake的設計,ID組成結構:正數位(佔1位元)+ 時間戳(佔41位元)+ 機器ID(佔5位元)+ 機房ID(佔5位元)+ 自增值(佔12位元),總共64位元組成的一個Long型別。

Leaf-snowflake不同於原始snowflake演算法地方,主要是在workId的生成上,Leaf-snowflake依靠Zookeeper生成workId,也就是上邊的機器ID(佔5位元)+ 機房ID(佔5位元)。Leaf中workId是基於ZooKeeper的順序Id來生成的,每個應用在使用Leaf-snowflake時,啟動時都會都在Zookeeper中生成一個順序Id,相當於一臺機器對應一個順序節點,也就是一個workId。

在這裡插入圖片描述
Leaf-snowflake啟動服務的過程大致如下:

  • 啟動Leaf-snowflake服務,連線Zookeeper,在leaf_forever父節點下檢查自己是否已經註冊過(是否有該順序子節點)。
  • 如果有註冊過直接取回自己的workerID(zk順序節點生成的int型別ID號),啟動服務。
  • 如果沒有註冊過,就在該父節點下面建立一個持久順序節點,建立成功後取回順序號當做自己的workerID號,啟動服務。

Leaf-snowflake對Zookeeper是一種弱依賴關係,除了每次會去ZK拿資料以外,也會在本機檔案系統上快取一個workerID檔案。一旦ZooKeeper出現問題,恰好機器出現故障需重啟時,依然能夠保證服務正常啟動。

啟動Leaf-snowflake模式也比較簡單,起動本地ZooKeeper,修改一下專案中的leaf.properties檔案,關閉leaf.segment模式,啟用leaf.snowflake模式即可。

leaf.segment.enable=false
#leaf.jdbc.url=jdbc:mysql://127.0.0.1:3306/xin-master?useUnicode=true&characterEncoding=utf8
#leaf.jdbc.username=junkang
#leaf.jdbc.password=junkang

leaf.snowflake.enable=true
leaf.snowflake.zk.address=127.0.0.1
leaf.snowflake.port=2181
    /**
     * 雪花演算法模式
     * @param key
     * @return
     */
    @RequestMapping(value = "/api/snowflake/get/{key}")
    public String getSnowflakeId(@PathVariable("key") String key) {
        return get(key, snowflakeService.getId(key));
    }

測試一下,訪問:http://127.0.0.1:8080/api/snowflake/get/leaf-segment-test

在這裡插入圖片描述
優點:

  • ID號碼是趨勢遞增的8byte的64位數字,滿足上述資料庫儲存的主鍵要求。

缺點:

  • 依賴ZooKeeper,存在服務不可用風險(實在不知道有啥缺點了)

三、Leaf監控

請求地址:http://127.0.0.1:8080/cache

針對服務自身的監控,Leaf提供了Web層的記憶體資料對映介面,可以實時看到所有號段的下發狀態。比如每個號段雙buffer的使用情況,當前ID下發到了哪個位置等資訊都可以在Web介面上檢視。

在這裡插入圖片描述

總結

對於Leaf具體使用哪種模式,還是根據具體的業務場景使用,本文並沒有對Leaf原始碼做過多的分析,因為Leaf 程式碼量簡潔很好閱讀。

原創不易,燃燒秀髮輸出內容,如果有一丟丟收穫,點個贊鼓勵一下吧!

整理了幾百本各類技術電子書,送給小夥伴們。關注公號回覆【666】自行領取。和一些小夥伴們建了一個技術交流群,一起探討技術、分享技術資料,旨在共同學習進步,如果感興趣就加入我們吧!

相關文章