分佈式唯一ID 雪花算法(JAVA)

唯一ID可以標識數據的唯一性,在分佈式系統中生成唯一ID的方案有很多,常見的方式大概有以下幾種:

  • 依賴數據庫,使用如MySQL自增列或Oracle序列等。
  • UUID隨機數
  • snowflake雪花算法(本文將要討論)
  • 利用Redis 單線程處理模型的自增長實現

本文主要講解Twitter的SnowFlake(雪化算法)

數據庫和UUID方案的不足之處

採用數據庫自增序列:

  • 讀寫分離時,只有主節點可以進行寫操作,可能有單點故障的風險
  • 分表分庫,數據遷移合併等比較麻煩

UUID隨機數:

  • 採用無意義字符串,沒有排序
  • UUID使用字符串形式存儲,數據量大時查詢效率比較低

雪花算法概述

雪花算法生成的ID是純數字且具有時間順序的。其原始版本是scala版,後面出現了許多其他語言的版本如Java、C++等。

結構

  • 最高位是符號位,始終爲0,不可用。
  • 41位的時間序列,精確到毫秒級,41位的長度可以使用69年。時間位還有一個很重要的作用是可以根據時間進行排序。
  • 10位的機器標識,10位的長度最多支持部署1024個節點
  • 12位的計數序列號,序列號即一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號

 

特點(自增、有序、適合分佈式場景)

  • 時間位:可以根據時間進行排序,有助於提高查詢速度。
  • 機器id位:適用於分佈式環境下對多節點的各個節點進行標識,可以具體根據節點數和部署情況設計劃分機器位10位長度,如劃分5位表示進程位等。
  • 序列號位:是一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號

優點

  • 靈活配置:機器碼可以根據需求靈活配置含義
  • 無需持久化:如果序號自增往往需要持久化,本算法不需要持久化
  • ID 有含義/可逆性:ID 可以反解出來,對 ID 進行統計分析,可以很簡單的分析出整個系統的繁忙曲線,還可以定位到每個機器,在某段時間承擔了多少工作,分析出負載均衡情況
  • 高性能:生成速度很快

雪花算法的缺點

雪花算法在單機系統上ID是遞增的,但是在分佈式系統多節點的情況下,所有節點的時鐘並不能保證不完全同步,所以有可能會出現不是全局遞增的情況。


/**
 * 分佈式唯一ID 雪花算法實現
 */
public class SnowFlake {
    /**
     * 起始的時間戳  2020-06-30 00:00:00
     */
    private final static long START_TIMESTAMP = 1593446400L;

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

    /**
     * 每一部分的最大值
     */
    private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_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 DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;

    /**
     * 數據中心編碼,初始化後不可修改
     * 最大值: 2^5-1 取值範圍: [0,31]
     */
    private long dataCenterId;

    /**
     * 機器或進程編碼,初始化後不可修改
     * 最大值: 2^5-1 取值範圍: [0,31]
     */
    private long machineId;

    /**
     * 序列號
     * 最大值: 2^12-1 取值範圍: [0,4095]
     */
    private long sequence = 0L;

    private long lastStamp = -1L;//上一次時間戳

    public SnowFlake(long dataCenterId, long machineId) {
        if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
            throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_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() {
        //獲取當前時間戳 可用System.currentTimeMillis(); 方法替換
        long currStamp = getNewStamp();
        //保證當前時間戳不小於最後一次的時間戳
        if (currStamp < lastStamp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
        //如果當前時間戳等於最後一次時間戳 就進行序號遞增
        if (currStamp == lastStamp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
                currStamp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置爲0
            sequence = 0L;
        }
        //保存最後一次時間戳
        lastStamp = currStamp;
        //通過位運算,將相應的二進制數值放到對應的位置  41位時間戳 + 5位數據中心 + 5位機器碼 + 12位序列號 ,首位沒有使用
        return (currStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //時間戳部分
                | dataCenterId << DATA_CENTER_LEFT       //數據中心部分
                | machineId << MACHINE_LEFT             //機器標識部分
                | sequence;                             //序列號部分
    }

    private long getNextMill() {
        //獲取當前時間戳
        long mill = getNewStamp();
        //獲取大於上一次時間戳的時間戳
        while (mill <= lastStamp) {
            mill = getNewStamp();
        }
        return mill;
    }

    /**
     * 獲取當前時間戳
     */
    private long getNewStamp() {
        return System.currentTimeMillis();
    }

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

        long start = System.currentTimeMillis();
        for (int i = 0; i < 100000; i++) {
            System.out.println(snowFlake.nextId());
        }

    }
}

總結

分佈式唯一ID的方案有很多,本文主要討論了雪花算法,組成結構大致分爲了無效位、時間位、機器位和序列號位。其特點是自增、有序、純數字組成查詢效率高且不依賴於數據庫。適合在分佈式的場景中應用,可根據需求調整具體實現細節。對於瞬時的高峯唯一ID請求,可以考慮結合Redis 隊列進行預生產大量ID,業務消費方優先從Redis獲取,ID生產方 持續寫入到Redis進行瞬時問題處理。

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