1. 程式人生 > 其它 >分散式全域性ID生成器原理剖析及非常齊全開源方案應用示例

分散式全域性ID生成器原理剖析及非常齊全開源方案應用示例

本篇文章著重介紹常用分散式全域性ID實現方案及原理和優缺點分析,通過Java程式碼示例使用開源框架百度Uidgenerator、美團Leaf、滴滴TinyID,這些都是大量用於生產實踐,而這些的開源分散式ID生成器設計思路基本都是基於號段模式和雪花演算法為基礎,根據不同業務場景做選擇,穩定且效能有保證,輕鬆使用解決分散式系統面臨的ID問題。

為何需要分散式ID生成器

**本人部落格網站 **IT小神 www.itxiaoshen.com

**拿我們系統常用Mysql資料庫來說,在之前的單體架構基本是單庫結構,每個業務表的ID一般從1增,通過 **AUTO_INCREMENT=1設定自增起始值,隨著系統(比如網際網路電商、外賣)使用者資料日漸增長,單庫效能無法滿足業務系統,在這之後我們會使用基於主從同步的讀寫分離,但當用戶量規模連主從模式都無法應對時,我們會採用分庫分表(當然現在還有其他解決方案比如分散式關係型資料庫如TiDB)的方案,這樣對資料分庫分表後需要有一個唯一 ID 來標識一條資料或訊息,資料庫的自增 ID 顯然不能滿足需求,在複雜分散式系統中,往往還有很多場景需要對大量的資料和訊息進行唯一標識,這就迫使我們需要用到分散式系統中全域性ID生成器。

我們本篇文章只是介紹一些常用實現方案,而大部分的開源分散式ID生成器基本都是基於號段模式和雪花演算法為基礎,可以根據不同業務場景需要選擇,不做詳細說明

分散式ID滿足要求

  • 全域性唯一:需要是唯一標識,不能出現重複的 ID 號,這是最基本的要求。
  • 高效能:高QPS、低延遲、否則反倒會成為系統瓶頸
  • 高可用性:可用性接近 5 個 9
  • 資訊保安:如果 ID 是連續的那對於惡意使用者爬蟲採用順序爬取指定 URL爬取資訊就非常容易完成;如果是作為訂單號就更危險了,可以直接知道一天的單量,所以在一些應用場景下會需要 ID 無規則、不規則的要求
  • 趨勢遞增:在 MySQL InnoDB 引擎中使用的是聚集索引,採用B+ Tree的資料結構來儲存索引資料,在主鍵的選擇上我們應該儘量使用有序的編號保證寫入效能
  • 單調遞增:保證下一個 ID 一定大於上一個 ID,例如事務版本號、IM 增量訊息、排序等特殊需求。

常用解決方案

UUID

全域性ID在Java中們可以簡單使用來UUID生成,輸出的41c9b76fc5ac4265939cd5b27bdacdf1這種結果的字串資料,可以看生成的是36位長度的16進位制的字串,然後將中劃線-替換為空字串**

public static void main(String[] args) { 
       String uuid = UUID.randomUUID().toString().replaceAll("-","");
       System.out.println(uuid);
 }

優點

  • 優點UUID設計上固然是可以滿足全域性唯一的要求

缺點

  • UUID太長且無序,在網際網路大部分企業中都是使用Mysql資料庫,且有些業務場景需要使用到事務因此底層儲存引擎採用的是Innodb,這就導致B+ Tree索引的分裂,儲存和索引的效能差,並不適合在Innodb作為主鍵,自增ID比較適合作為Innodb主鍵

資料庫自增ID

這樣方式就是單獨使用一個數據庫來生成ID,業務程式通過這個資料庫獲取ID,表結構可以簡單設計如下,--然後再通過事務通過插入等操作資料觸發ID自增,這個資料庫層級效能比較高,你也可以採用表級別插入返回資料的主鍵

CREATE DATABASE `SEQ_ID`;
CREATE TABLE SEQID.SEQUENCE_ID (
    id bigint(20) unsigned NOT NULL auto_increment, 
    id_value char(10) NOT NULL default '',
    PRIMARY KEY (id),
    UNIQUE KEY id_value(id_value)
) ENGINE=MyISAM;
begin
    replace into SEQUENCE_ID(id_value) values('xxx'); 
    SELECT LAST_INSERT_ID();
    commit;
end

優點

  • 簡單、ID自增

缺點

  • DB單點故障
  • Mysql併發不好,無法抗住高併發

資料庫叢集模式

上面單個數據庫有弊端,那麼可以採用資料庫叢集,資料庫叢集常用主從和主主,我們使用主主模式,每個資料庫通過設定不同起始值和相同自增步長來實現,比如三臺mysql主主模式,mysql1從1開始自增步長為3,序號1、4、7...,mysql2從2開始自增步長為3,序號2、5、8...,mysql3從3開始自增步長為3,序號3、6、9....,每個業務系統可以通過這三臺中獲取到ID

set @@auto_increment_offset = 1;     -- mysql1起始值
set @@auto_increment_increment = 3;  -- mysql1自增步長

set @@auto_increment_offset = 2;     -- mysql2起始值
set @@auto_increment_increment = 3;  -- mysql2自增步長

set @@auto_increment_offset = 3;     -- mysql3起始值
set @@auto_increment_increment = 3;  -- mysql3自增步長

優點

  • 解決DB單點問題

缺點

  • 不利於擴容,如果需要進行MySQL擴容增加節點還是比較麻煩,可能還需要停機擴容

號段模式

號段模式幾乎是目前所有開源分散式ID生成器的主流實現方式之一,號段模式比如每次從資料庫取出一個號段範圍,例如 (1,1000] 代表1000個ID,具體的業務服務將本號段,生成1~1000的自增ID並載入到記憶體,不強依賴於資料庫,不會頻繁的訪問資料庫,對資料庫的壓力小很多。簡易版本的表結構如下:

CREATE TABLE id_generator (
  id int(10) NOT NULL,
  max_id bigint(20) NOT NULL COMMENT '當前最大id',
  step int(20) NOT NULL COMMENT '號段的步長',
  biz_type    int(20) NOT NULL COMMENT '業務型別',
  version int(20) NOT NULL COMMENT '版本號',
  PRIMARY KEY (`id`)
) 

biz_type :代表不同業務型別

max_id :當前最大的可用id

step :代表號段的長度

version :是一個樂觀鎖,每次都更新version,保證併發時資料的正確性

每次申請一個號段,通過樂觀鎖的機制對 max_id欄位做一次 update操作,update成功則說明新號段獲取成功,新的號段範圍是 (max_id ,max_id +step]

update id_generator set max_id = #{max_id+step}, version = version + 1 where version = # {version} and biz_type = XXX

Redis實現

Redis也同樣可以實現,原理就是利用 redis**的 **incr命令實現ID的原子性自增,redis持久化也支援基於每條命令持久化方式,且redis自身有高可用叢集模式

192.168.3.117:6379> set seq_id 1     // 初始化自增ID為1
OK
192.168.3.117:6379> incr seq_id      // 增加1,並返回遞增後的數值
(integer) 2

雪花演算法(SnowFlake)

雪花演算法(Snowflake)是twitter公司內部分散式專案採用的ID生成演算法,開源後廣受國內大廠的好評,在該演算法影響下各大公司相繼開發出各具特色的分散式生成器。SnowFlake演算法用來生成64位的ID,剛好可以用long整型儲存,能夠用於分散式系統中生產唯一的ID, 並且生成的ID有序

Snowflake生成的是Long型別的ID,一個Long型別佔8個位元組,每個位元組佔8位元,也就是說一個Long型別佔64個位元。

Snowflake ID組成結構:正數位(佔1位元)+ 時間戳(佔41位元)+ 機器ID(佔5位元)+ 資料中心(佔5位元)+ 自增值(佔12位元),總共64位元組成的一個Long型別。

  • 第一個bit位(1bit):Java中long的最高位是符號位代表正負,正數是0,負數是1,一般生成ID都為正數,所以預設為0。
  • 時間戳部分(41bit):毫秒級的時間,不建議存當前時間戳,而是用(當前時間戳 - 固定開始時間戳)的差值,可以使產生的ID從更小的值開始;41位的時間戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年
  • 工作機器id(10bit):也被叫做workId,這個可以靈活配置,機房或者機器號組合都可以。
  • 序列號部分(12bit),自增值支援同一毫秒內同一個節點可以生成4096個ID

雪花演算法比較依賴於時間,會出現時鐘回撥的問題,所以儘量保證時間同步,大部分開源分散式ID生成器大都有優化解決時鐘回撥的問題

雪花演算法Java實現原始碼Gitub地址

下面是基於Twitter的雪花演算法SnowFlake,使用Java語言實現,封裝成工具方法,各個業務應用可以直接使用該工具方法來獲取分散式ID,只需保證每個業務應用有自己的工作機器id即可,而不需要單獨去搭建一個獲取分散式ID的應用

0 - 41位時間戳 - 5位資料中心標識 - 5位機器標識 - 12位序列號

5位資料中心標識跟5位機器標識這樣的分配僅僅是當前實現中分配的,如果業務有其實的需要,可以按其它的分配比例分配,如10位機器標識,不需要資料中心標識。

/**
 * twitter的snowflake演算法 -- java實現
 * 
 * @author beyond
 * @date 2016/11/26
 */
public class SnowFlake {

    /**
     * 起始的時間戳
     */
    private final static long START_STMP = 1480166465631L;

    /**
     * 每一部分佔用的位數
     */
    private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
    private final static long MACHINE_BIT = 5;   //機器標識佔用的位數
    private final static long DATACENTER_BIT = 5;//資料中心佔用的位數

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private long datacenterId;  //資料中心
    private long machineId;     //機器標識
    private long sequence = 0L; //序列號
    private long lastStmp = -1L;//上一次時間戳

    public SnowFlake(long datacenterId, long machineId) {
        if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) {
            throw new IllegalArgumentException("datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0");
        }
        if (machineId > MAX_MACHINE_NUM || machineId < 0) {
            throw new IllegalArgumentException("machineId can't be greater than MAX_MACHINE_NUM or less than 0");
        }
        this.datacenterId = datacenterId;
        this.machineId = machineId;
    }

    /**
     * 產生下一個ID
     *
     * @return
     */
    public synchronized long nextId() {
        long currStmp = getNewstmp();
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }

        if (currStmp == lastStmp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置為0
            sequence = 0L;
        }

        lastStmp = currStmp;

        return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | datacenterId << DATACENTER_LEFT       //資料中心部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }

    private long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private long getNewstmp() {
        return System.currentTimeMillis();
    }

    public static void main(String[] args) {
        SnowFlake snowFlake = new SnowFlake(2, 3);

        for (int i = 0; i < (1 << 12); i++) {
            System.out.println(snowFlake.nextId());
        }

    }
}

百度 (Uidgenerator)

概述

官方GitHub地址** **https://github.com/baidu/uid-generator

UidGenerator是Java實現的, 基於Snowflake演算法的唯一ID生成器。UidGenerator以元件形式工作在應用專案中, 支援自定義workerId位數和初始化策略, 從而適用於docker等虛擬化環境下例項自動重啟、漂移等場景。 在實現上, UidGenerator通過借用未來時間來解決sequence天然存在的併發限制; 採用RingBuffer來快取已生成的UID, 並行化UID的生產和消費, 同時對CacheLine補齊,避免了由RingBuffer帶來的硬體級「偽共享」問題. 最終單機QPS可達600萬。

依賴版本:Java8及以上版本, MySQL(內建WorkerID分配器, 啟動階段通過DB進行分配; 如自定義實現, 則DB非必選依賴)

[](https://github.com/baidu/uid-generator/blob/master/doc/snowflake.png)

Snowflake演算法描述:指定機器 & 同一時刻 & 某一併發序列,是唯一的。據此可生成一個64 bits的唯一ID(long)。預設採用上圖位元組分配方式:

  • sign(1bit)****固定1bit符號標識,即生成的UID為正數。
  • delta seconds (28 bits)****當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,而不是毫秒,最多可支援約8.7年
  • worker id (22 bits)****機器id,最多可支援約420w次機器啟動。內建實現為在啟動時由資料庫分配,預設分配策略為用後即棄,後續可提供複用策略,同一應用每次重啟就會消費一個workId
  • sequence (13 bits)**
    **每秒下的併發序列,13 bits可支援每秒8192個併發。

UidGenerator是基於 Snowflake演算法實現的,與原始的 snowflake演算法不同在於,UidGenerator支援自 定義時間戳工作機器ID序列號 等各部分的位數,而且 UidGenerator中採用使用者自定義 workId的生成策略。

UidGenerator需要與資料庫配合使用,需要新增一個 WORKER_NODE表。當應用啟動時會向資料庫表中去插入一條資料,插入成功後返回的自增ID就是該機器的 workId資料由host,port組成。

提供了兩種生成器: DefaultUidGenerator、CachedUidGenerator,如對UID生成效能有要求則使用CachedUidGenerator。

CachedUidGenerator

RingBuffer環形陣列,陣列每個元素成為一個slot。RingBuffer容量,預設為Snowflake演算法中sequence最大值,且為2^N。可通過 boostPower配置進行擴容,以提高RingBuffer 讀寫吞吐量。

Tail指標、Cursor指標用於環形陣列上讀寫slot:

  • Tail指標****表示Producer生產的最大序號(此序號從0開始,持續遞增)。Tail不能超過Cursor,即生產者不能覆蓋未消費的slot。當Tail已趕上curosr,此時可通過rejectedPutBufferHandler指定PutRejectPolicy
  • Cursor指標**
    **表示Consumer消費到的最小序號(序號序列與Producer序列相同)。Cursor不能超過Tail,即不能消費未生產的slot。當Cursor已趕上tail,此時可通過rejectedTakeBufferHandler指定TakeRejectPolicy

CachedUidGenerator採用了雙RingBuffer,Uid-RingBuffer用於儲存Uid、Flag-RingBuffer用於儲存Uid狀態(是否可填充、是否可消費)

由於陣列元素在記憶體中是連續分配的,可最大程度利用CPU cache以提升效能。但同時會帶來「偽共享」FalseSharing問題,為此在Tail、Cursor指標、Flag-RingBuffer中採用了CacheLine 補齊方式。

RingBuffer填充時機

  • 初始化預填充****RingBuffer初始化時,預先填充滿整個RingBuffer.
  • 即時填充****Take消費時,即時檢查剩餘可用slot量(tail** -cursor),如小於設定閾值,則補全空閒slots。閾值可通過paddingFactor來進行配置,請參考Quick Start中CachedUidGenerator配置**
  • 週期填充**
    **通過Schedule執行緒,定時補全空閒slots。可通過scheduleInterval配置,以應用定時填充功能,並指定Schedule時間間隔

簡單使用

官方原始碼匯入idea

建立資料庫和匯入表WORKER_NODE.sql

建立一個SpringBoot啟動類,在application-dev.yml檔案配置資料庫資訊,啟動類配置Mybatis掃描com.baidu.fsg.uid的mapper檔案註解,建立一個UidControoler提供一個獲取單個uid的介面,啟動SpringBoot程式

訪問提供介面地址:http://localhost:8080/uid/snowflake** ,返回uid結果,每次重新整理+1**

資料庫表WORKER_NODE當我們每次啟動程式會重新生成新的記錄

美團(Leaf)

概述

官方GitHub地址** **https://github.com/Meituan-Dianping/Leaf

There are no two identical leaves in the world. 世界上沒有兩片完全相同的樹葉。

— 萊布尼茨

Leaf 最早期需求是各個業務線的訂單ID生成需求。在美團早期,有的業務直接通過DB自增的方式生成ID,有的業務通過redis快取來生成ID,也有的業務直接用UUID這種方式來生成ID。以上的方式各自有各自的問題,因此我們決定實現一套分散式ID生成服務來滿足需求。

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

當然,為了追求更高的效能,需要通過RPC Server來部署Leaf 服務,那僅需要引入leaf-core的包,把生成ID的API封裝到指定的RPC框架中即可。

Leaf Server 是一個spring boot的程式,提供HTTP服務來獲取ID。

Leaf 提供兩種生成的ID的方式(號段模式和snowflake模式),你可以同時開啟兩種方式,也可以指定開啟某種方式(預設兩種方式為關閉狀態)

配置

Leaf Server的配置都在leaf-server/src/main/resources/leaf.properties中

配置項 含義 預設值
leaf.name leaf 服務名
leaf.segment.enable 是否開啟號段模式 false
leaf.jdbc.url mysql 庫地址
leaf.jdbc.username mysql 使用者名稱
leaf.jdbc.password mysql 密碼
leaf.snowflake.enable 是否開啟snowflake模式 false
leaf.snowflake.zk.address snowflake模式下的zk地址
leaf.snowflake.port snowflake模式下的服務註冊埠
  • 號段模式
    • 如果使用號段模式,需要建立DB表,並配置leaf.jdbc.url, leaf.jdbc.username, leaf.jdbc.password
    • 如果不想使用該模式配置leaf.segment.enable=false即可。
  • Snowflake模式
    • 演算法取自twitter開源的snowflake演算法。
    • 如果不想使用該模式配置leaf.snowflake.enable=false即可。
    • 配置zookeeper地址
      • 在leaf.properties中配置leaf.snowflake.zk.address,配置leaf 服務監聽的埠leaf.snowflake.port。

簡單使用

  • 建立資料庫,通過原始碼根目錄下的scripts的leaf_alloc.sql匯入資料庫表leaf_alloc
  • 初始化資料,設定步長為2000,每次重啟重新獲取為下一個號段起始值
INSERT INTO leaf_alloc(biz_tag, max_id, step, DESCRIPTION) VALUES('itxs', 1, 2000, 'Test leaf Segment Mode Get Id')

配置application.properties中的資料庫資訊,將leaf.segment.enable設定為true或者註釋;配置zookeeper資訊,leaf.snowflake.enable設定為true或者註釋;啟動leaf-server Spring Boot啟動類

訪問號段模式http介面地址:http://localhost:8080/api/segment/get/itxs

訪問雪花演算法的http介面地址:http://localhost:8080/api/snowflake/get/test

訪問監控頁面地址:http://localhost:8080/cache

我們再使用上一小節的工程專案先簡單通過將leaf的core模組原始碼工程引入,使用號段模式,通過@Autowired SegmentIDGenImpl主動注入leaf號段模式實現類,並完成http getSegment測試介面的controller

package com.itxs.uiddemo.controller;

import javax.annotation.Resource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.baidu.fsg.uid.UidGenerator;
import com.sankuai.inf.leaf.Result;
import com.sankuai.inf.leaf.segment.SegmentIDGenImpl;

@RestController
@RequestMapping(value="/uid")
public class UidController {

	@Resource(name = "cachedUidGenerator")
    private UidGenerator cachedUidGenerator;

	@Autowired
	private SegmentIDGenImpl idGen;

    @GetMapping("/snowflake")
    public String snowflake() {
        return String.valueOf(this.cachedUidGenerator.getUID());
    }
  
    @GetMapping(value = "/segment/{key}")
    public Result<Long> getSegment(@PathVariable("key") String key) throws Exception {
    	return this.idGen.get(key);
    }
  
}

啟動Spring Boot程式,訪問http://localhost:8080/uid/segment/itxs,返回data欄位就是uid值,每次重新整理+1

重新啟動後,再次訪問http://localhost:8080/uid/segment/itxs,返回data欄位1001,也即是新的號段的起始值,資料庫的maxid也變為1001

當然也可以採用Spring Boot Startser方式使用,官網也有相關的說明

我們自己下載leaf-starter 整合Spring Boot 製作啟動器starter原始碼進行編譯

編譯好leaf-boot-starter後我們新建一個Spring Boot demo工程,由於原來封裝是基於Spring Boot早期的版本,高版本不相容,所以用早期版本,由於leaf-boot-starter裡面使用zookeeper的客戶端curator,我們直接執行是出現curator的某些類找不到,因此我們簡單就直接在工程加入curator-framework和curator-recipes的依賴。

pom檔案

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.itxs</groupId>
    <artifactId>leaf-spring-boot-demo</artifactId>
    <version>1.0-SNAPSHOT</version>
    <parent>
        <artifactId>spring-boot-starter-parent</artifactId>
        <groupId>org.springframework.boot</groupId>
        <version>2.0.3.RELEASE</version>
    </parent>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>com.sankuai.inf.leaf</groupId>
            <artifactId>leaf-boot-starter</artifactId>
            <version>1.0.1-RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>5.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>5.2.0</version>
        </dependency>
    </dependencies>
</project>

在class path也即是resource根目錄下新建leaf.properties檔案,同時開啟號段模式和雪花演算法,配置資訊如下

leaf.name=com.sankuai.leaf.opensource.test
leaf.segment.enable=true
leaf.segment.url=jdbc:mysql://192.168.3.117:3306/leaf?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
leaf.segment.username=leaf
leaf.segment.password=leaf123
leaf.snowflake.enable=true
leaf.snowflake.address=192.168.3.117
leaf.snowflake.port=2181

新建一個controller用於測試,提供號段和雪花演算法測試介面

package com.itxs.controller;

import com.sankuai.inf.leaf.common.Result;
import com.sankuai.inf.leaf.service.SegmentService;
import com.sankuai.inf.leaf.service.SnowflakeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping(value="/uid")
public class LeafUidController {

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

    @GetMapping("/snowflake")
    public String snowflake() {
        return String.valueOf(this.snowflakeService.getId("test"));
    }
  
    @GetMapping(value = "/segment/{key}")
    public Result getSegment(@PathVariable("key") String key) throws Exception {
    	return this.segmentService.getId(key);
    }
}

新建Spring Boot啟動類,在啟動類上標註@EnableLeafServer開啟LeafServer的註解,啟動Spring Boot程式,預設是使用8080埠

package com.itxs;

import com.sankuai.inf.leaf.plugin.annotation.EnableLeafServer;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@EnableLeafServer
public class LeafApplication {
    public static void main(String[] args) {
        SpringApplication.run(LeafApplication.class,args);
    }
}

訪問號段uid獲取介面:http://localhost:8080/uid/segment/itxs,放回id結果如下

訪問雪花演算法uid獲取介面:http://localhost:8080/uid/snowflake,返回id結果如下

滴滴(TinyID)

概述

官方GitHub地址** **https://github.com/didi/tinyid/

Tinyid是用Java開發的一款分散式id生成系統,基於資料庫號段演算法實現,關於這個演算法可以參考美團leaf或者tinyid原理介紹。Tinyid擴充套件了leaf-segment演算法,支援了多db(master),同時提供了java-client(sdk)使id生成本地化,獲得了更好的效能與可用性。Tinyid在滴滴客服部門使用,均通過tinyid-client方式接入,每天生成億級別的id。

  • 效能
    • http方式訪問,效能取決於http server的能力,網路傳輸速度
    • java-client方式,id為本地生成,號段長度(step)越長,qps越大,如果將號段設定足夠大,則qps可達1000w+
  • 可用性
    • 依賴db,當db不可用時,因為server有快取,所以還可以使用一段時間,如果配置了多個db,則只要有1個db存活,則服務可用
    • 使用tiny-client,只要server有一臺存活,則理論上可用,server全掛,因為client有快取,也可以繼續使用一段時間
  • 特性
    • 全域性唯一的long型id
    • 趨勢遞增的id,即不保證下一個id一定比上一個大
    • 非連續性
    • 提供http和java client方式接入
    • 支援批量獲取id
    • 支援生成1,3,5,7,9...序列的id
    • 支援多個db的配置,無單點

適用場景:只關心id是數字,趨勢遞增的系統,可以容忍id不連續,有浪費的場景**
**不適用場景:類似訂單id的業務(因為生成的id大部分是連續的,容易被掃庫、或者測算出訂單量)

推薦使用方式

  • tinyid-server推薦部署到多個機房的多臺機器
    • 多機房部署可用性更高,http方式訪問需使用方考慮延遲問題
  • 推薦使用tinyid-client來獲取id,好處如下:
    • id為本地生成(呼叫AtomicLong.addAndGet方法),效能大大增加
    • client對server訪問變的低頻,減輕了server的壓力
    • 因為低頻,即便client使用方和server不在一個機房,也無須擔心延遲
    • 即便所有server掛掉,因為client預載入了號段,依然可以繼續使用一段時間 注:使用tinyid-client方式,如果client機器較多頻繁重啟,可能會浪費較多的id,這時可以考慮使用http方式
  • 推薦db配置兩個或更多:
    • db配置多個時,只要有1個db存活,則服務可用 多db配置,如配置了兩個db,則每次新增業務需在兩個db中都寫入相關資料

原理和架構

  • tinyid是基於資料庫發號演算法實現的,簡單來說是資料庫中儲存了可用的id號段,tinyid會將可用號段載入到記憶體中,之後生成id會直接記憶體中產生。
  • 可用號段在第一次獲取id時載入,如當前號段使用達到一定量時,會非同步載入下一可用號段,保證記憶體中始終有可用號段。
  • (如可用號段1-1000被載入到記憶體,則獲取id時,會從1開始遞增獲取,當使用到一定百分比時,如20%(預設),即200時,會非同步載入下一可用號段到記憶體,假設新載入的號段是1001-2000,則此時記憶體中可用號段為200-1000,1001~2000),當id遞增到1000時,當前號段使用完畢,下一號段會替換為當前號段。依次類推。
  • nextId和getNextSegmentId是tinyid-server對外提供的兩個http介面
  • nextId是獲取下一個id,當呼叫nextId時,會傳入bizType,每個bizType的id資料是隔離的,生成id會使用該bizType型別生成的IdGenerator。
  • getNextSegmentId是獲取下一個可用號段,tinyid-client會通過此介面來獲取可用號段
  • IdGenerator是id生成的介面
  • IdGeneratorFactory是生產具體IdGenerator的工廠,每個biz_type生成一個IdGenerator例項。通過工廠,我們可以隨時在db中新增biz_type,而不用重啟服務
  • IdGeneratorFactory實際上有兩個子類IdGeneratorFactoryServer和IdGeneratorFactoryClient,區別在於,getNextSegmentId的不同,一個是DbGet,一個是HttpGet
  • CachedIdGenerator則是具體的id生成器物件,持有currentSegmentId和nextSegmentId物件,負責nextId的核心流程。nextId最終通過AtomicLong.andAndGet(delta)方法產生。

簡單使用

  • 建立表
    • 匯入原始碼根目錄下面tinyid/tinyid-server/db.sql的資料庫指令碼,兩張表一張儲存每個業務型別的token授權資訊,一張儲存業務型別ID的號段模式起始值和步長,通過version也即是資料庫樂觀鎖實現原子操作。
cd tinyid/tinyid-server/ && create table with db.sql (mysql)
  • 配置db
cd tinyid-server/src/main/resources/offline
vi application.properties
datasource.tinyid.names=primary
datasource.tinyid.primary.driver-class-name=com.mysql.jdbc.Driver
datasource.tinyid.primary.url=jdbc:mysql://ip:port/databaseName?autoReconnect=true&useUnicode=true&characterEncoding=UTF-8
datasource.tinyid.primary.username=root
datasource.tinyid.primary.password=123456
  • 啟動tinyid-server
    • 將原始碼放在一個linux主機上,當然得有Jdk和Maven環境,在tinyid-server目錄下執行指令碼編譯並啟動編譯好的jar包.並啟動tinyid-server程式
cd tinyid-server/
sh build.sh offline
java -jar output/tinyid-server-xxx.jar

或者將tinyid原始碼匯入idea中,同樣配置db,然後啟動tinyid-server

通過初始化sql指令碼中的授權碼和biz_type,訪問本地的RestApi介面測試,結果如下

接下來我們使用基於java客戶端的方式,這也是官方推薦的,效能最好,我們這裡就直接使用客戶端原始碼工程的測試程式碼

  • 匯入Maven dependency
<dependency>
    <groupId>com.xiaoju.uemc.tinyid</groupId>
    <artifactId>tinyid-client</artifactId>
    <version>${tinyid.version}</version>
</dependency>
  • 配置客戶端資訊tinyid_client.properties
tinyid.server=localhost:9999
tinyid.token=0f673adf80504e2eaa552f5d791b644c

#(tinyid.server=localhost:9999/gateway,ip2:port2/prefix,...)
  • 編寫程式碼,test為業務型別
Long id = TinyId.nextId("test");
List<Long> ids = TinyId.nextId("test", 10);

我們再看資料庫表的資訊,發現max_id已經變為200001,也即是每個客戶端通過步長申請號段放在記憶體中,然後更新資料庫表為下一次申請id段的起始值

看到這裡,以後如果遇到需要使用分散式ID的場景,你會選擇和使用了嗎?