溫故知新-分佈式-分佈式Id生成算法&踩過的坑


分佈式id特點

  • 唯一性:確保生成的ID是全網唯一的。
  • 有序遞增性:確保生成的ID是對於某個用戶或者業務是按一定的數字有序遞增的。
  • 高可用性:確保任何時候都能正確的生成ID。
  • 帶時間:ID裏面包含時間。

常見的分佈式id算法比較

UUID

算法的核心思想是結合機器的網卡、當地時間、一個隨記數來生成UUID。

  • 優點:本地生成,生成簡單,性能好,沒有高可用風險

  • 缺點:長度過長,存儲冗餘,且無序不可讀,查詢效率低

數據庫自增ID

使用數據庫的id自增策略,如 MySQL 的 auto_increment。並且可以使用兩臺數據庫分別設置不同步長,生成不重複ID的策略來實現高可用。

  • 優點:數據庫生成的ID絕對有序,高可用實現方式簡單
  • 缺點:需要獨立部署數據庫實例,成本高,有性能瓶頸

批量生成ID

一次按需批量生成多個ID,每次生成都需要訪問數據庫,將數據庫修改爲最大的ID值,並在內存中記錄當前值及最大值。

  • 優點:避免了每次生成ID都要訪問數據庫並帶來壓力,提高性能
  • 缺點:屬於本地生成策略,存在單點故障,服務重啓造成ID不連續

Redis生成ID

Redis的所有命令操作都是單線程的,本身提供像 incr 和 increby 這樣的自增原子命令,所以能保證生成的 ID 肯定是唯一有序的。

  • 優點:不依賴於數據庫,靈活方便,且性能優於數據庫;數字ID天然排序,對分頁或者需要排序的結果很有幫助。
  • 缺點:如果系統中沒有Redis,還需要引入新的組件,增加系統複雜度;需要編碼和配置的工作量比較大。

考慮到單節點的性能瓶頸,可以使用 Redis 集羣來獲取更高的吞吐量。假如一個集羣中有5臺 Redis。可以初始化每臺 Redis 的值分別是1, 2, 3, 4, 5,然後步長都是 5。各個 Redis 生成的 ID 爲:

A1, 6, 11, 16, 21
B2, 7, 12, 17, 22
C3, 8, 13, 18, 23
D4, 9, 14, 19, 24
E5, 10, 15, 20, 25

隨便負載到哪個機確定好,未來很難做修改。步長和初始值一定需要事先確定。使用 Redis 集羣也可以方式單點故障的問題。

另外,比較適合使用 Redis 來生成每天從0開始的流水號。比如訂單號 = 日期 + 當日自增長號。可以每天在 Redis 中生成一個 Key ,使用 INCR 進行累加。

Twitter的snowflake算法

Twitter 利用 zookeeper 實現了一個全局ID生成的服務 Snowflake:

image-20200615140243538

如上圖的所示,Twitter 的 Snowflake 算法由下面幾部分組成:

  • 1位符號位:

由於 long 類型在 java 中帶符號的,最高位爲符號位,正數爲 0,負數爲 1,且實際系統中所使用的ID一般都是正數,所以最高位爲 0。

  • 41位時間戳(毫秒級):

需要注意的是此處的 41 位時間戳並非存儲當前時間的時間戳,而是存儲時間戳的差值(當前時間戳 - 起始時間戳),這裏的起始時間戳一般是ID生成器開始使用的時間戳,由程序來指定,所以41位毫秒時間戳最多可以使用 (1 << 41) / (1000x60x60x24x365) = 69年

  • 10位數據機器位:

包括5位數據標識位和5位機器標識位,這10位決定了分佈式系統中最多可以部署 1 << 10 = 1024 s個節點。超過這個數量,生成的ID就有可能會衝突。

  • 12位毫秒內的序列:

這 12 位計數支持每個節點每毫秒(同一臺機器,同一時刻)最多生成 1 << 12 = 4096個ID

加起來剛好64位,爲一個Long型。

  • 優點:高性能,低延遲,按時間有序,一般不會造成ID碰撞

  • 缺點:需要獨立的開發和部署,依賴於機器的時鐘

百度UidGenerator

UidGenerator是百度開源的分佈式ID生成器,基於於snowflake算法的實現,看起來感覺還行。不過,國內開源的項目維護性真是擔憂。

具體可以參考官網說明:github.com/baidu/uid-g…

美團Leaf

Leaf 是美團開源的分佈式ID生成器,能保證全局唯一性、趨勢遞增、單調遞增、信息安全,裏面也提到了幾種分佈式方案的對比,但也需要依賴關係數據庫、Zookeeper等中間件。

具體可以參考官網說明:tech.meituan.com/MT_Leaf.htm…

設計一個id生成算法

主要方案

參考twitter的snowflake算法,id生成方案:

方案:1符號位+41位時間戳+(10位ip段最後一位)+12位毫秒級自增序列

詳細代碼可以參考一下這個地址,再加上ip的獲取即可,地址:https://github.com/beyondfengyu/SnowFlake/blob/master/SnowFlake.java

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());
        }

    }
}

踩過的坑

重點:如果你們的系統中有使用id生成算法作爲分庫分表鍵時,如果考慮方便擴容,分庫分表設置的2^N時,可能會導致hash數據分佈非常不均勻,此時需要對這段代碼進行改造,防止每一毫秒的都從0開始自增;當然,如果使用一個素數做爲取模的值時,那就沒有必要修改了;

image-20200615151404586

參考

Leaf——美團點評分佈式ID生成系統

snowFlske算法


你的鼓勵也是我創作的動力

打賞地址

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章