常用的分布式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

image-20210829210102638

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

由於數組元素在內存中是連續分配的,可最大程度利用CPU cache以提升性能。但同時會帶來「偽共享」FalseSharing問題,為此在Tail、Cursor指針、Flag-RingBuffer中采用了CacheLine 補齊方式。

image-20210829210153698

RingBuffer填充時機

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

簡單使用

官方源碼導入idea

image-20210829204014734

建立數據庫和導入表WORKER_NODE.sql

image-20210829201135735

創建一個SpringBoot啟動類,在application-dev.yml文件配置數據庫信息,啟動類配置Mybatis掃描com.baidu.fsg.uid的mapper文件注解,創建一個UidControoler提供一個獲取單個uid的接口,啟動SpringBoot程序

image-20210829205007068

訪問提供接口地址:http://localhost:8080/uid/snowflake** ,返回uid結果,每次刷新+1**

image-20210829205318046

數據庫表WORKER_NODE當我們每次啟動程序會重新生成新的記錄

image-20210829205247880

美團(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

image-20210829203224526

  • 初始化數據,設置步長為2000,每次重啟重新獲取為下一個號段起始值
INSERT INTO leaf_alloc(biz_tag, max_id, step, DESCRIPTION) VALUES('itxs', 1, 2000, 'Test leaf Segment Mode Get Id')

image-20210829215839396

配置application.properties中的數據庫信息,將leaf.segment.enable設置為true或者注釋;配置zookeeper信息,leaf.snowflake.enable設置為true或者注釋;啟動leaf-server Spring Boot啟動類

image-20210830100052982

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

image-20210830102426517

訪問雪花算法的http接口地址:http://localhost:8080/api/snowflake/get/test

image-20210830103151706

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

image-20210830103714201

我們再使用上一小節的工程項目先簡單通過將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

image-20210829221009674

image-20210829221109833

重新啟動后,再次訪問http://localhost:8080/uid/segment/itxs,返回data字段1001,也即是新的號段的起始值,數據庫的maxid也變為1001

image-20210829221248760

image-20210829221231614

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

image-20210829221523286

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

image-20210830105812373

image-20210830105747428

編譯好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);
    }
}

image-20210830124548952

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

image-20210830125150387

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

image-20210830125205414

滴滴(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時,當前號段使用完畢,下一號段會替換為當前號段。依次類推。

image-20210829214432009

  • 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)

image-20210830004523915

  • 配置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

image-20210830093048497

或者將tinyid源碼導入idea中,同樣配置db,然后啟動tinyid-server

image-20210830093235082

通過初始化sql腳本中的授權碼和biz_type,訪問本地的RestApi接口測試,結果如下

image-20210830092807922

image-20210830093334589

image-20210830093402860

接下來我們使用基於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);

image-20210830094103356

我們再看數據庫表的信息,發現max_id已經變為200001,也即是每個客戶端通過步長申請號段放在內存中,然后更新數據庫表為下一次申請id段的起始值

image-20210830093814125

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


免責聲明!

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



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