分散式 ID 生成演算法 — SnowFlake

JMCui發表於2021-01-21

一、概述

分散式 ID 生成演算法的有很多種,Twitter 的 SnowFlake 就是其中經典的一種。

SnowFlake 演算法生成 ID 的結果是一個 64bit 大小的整數,它的結構如下圖:

  • 1 位,不用。二進位制中最高位為 1 的都是負數,但是我們生成的 id 一般都使用整數,所以這個最高位固定是 0。
  • 41 位,用來記錄時間戳(毫秒)。41 位可以表示 2^41 個數字;如果只用來表示正整數(計算機中正數包含 0),可以表示的數值範圍是:0 至 2^41−1,也就是說 41 位可以表示 2^41 個毫秒的值,轉化成單位年則是 2^41 / (1000 * 60 * 60 * 24 * 365) = 69年。
  • 10 位,用來記錄工作機器 id,可以部署在 2^10 = 1024個節點,包括 5 位 datacenterId 和 5 位 workerId ;5 位(bit)可以表示的最大正整數是2^5-1 = 31,即可以用 0、1、2、3、....31 這 32 個數字,來表示不同的 datecenterId 或 workerId。
  • 12 位,序列號,用來記錄同毫秒內產生的不同 id。12位(bit)可以表示的最大正整數是 2^12-1 =4095,即可以用 0、1、2、3、....4095 這 4096 個數字,來表示同一機器同一時間截(毫秒)內產生的 4096 個ID序號。

由於在 Java 中 64 bit 的整數是 Long 型別,所以在 Java 中 SnowFlake 演算法生成的 ID 就是 Long 來儲存的。

SnowFlake 可以保證:

  • 所有生成的 ID 按時間趨勢遞增;
  • 整個分散式系統內不會產生重複id(因為有 datacenterId 和 workerId 來做區分);

二、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 << DATACENTER_BIT);
    private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
    private final static long MAX_SEQUENCE = ~(-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);

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

三、SnowFlake 演算法的生產實踐

通過前面的講述我們知道 SnowFlake 演算法能產生全域性唯一 ID 主要是通過 5 位 datacenterId 和 5 位 workerId 來做區分,而在生產實踐中,我們常常將機器 ID(hostid)來當成 datacenterId,將機器上執行的例項ID(pid)來當成 workerId。

如今的分散式部署都在強調無狀態化,那麼給每臺機器繫結一個 hostid 顯然不太現實,假設又是在容器化環境下,沒有固定的 IP,並且容器例項每次重新啟動後的唯一 ID 還不一致。綜上,基於機器 ID 的思路不可行。所以經常的做法是,利用 ZK/Redis/DB 等作為一個全域性的 datacenterId/workerId 發號器,由發號器來分配唯一的 datacenterId/workerId 。(可以參考百度分散式唯一 ID 生成器 UidGenerator)

四、SnowFlake 演算法的問題思考

1. 時間回撥問題

由於機器的時間是動態的調整的,有可能會出現時間跑到之前幾毫秒,如果這個時候獲取到了這種時間,則會出現資料重複。

這個問題的解決方案是採用“歷史時間”。在程式啟動後,我們會將當前時間(實際處理採用了延遲10ms啟動),作為該業務這臺機器程式的時間戳中的起始時間欄位,後續的自增是在序列號自增到最大值時,時間戳增 1,而序列號重新歸為 0。

2. 機器 id 分配及回收

目前機器 id 需要每臺機器不一樣,這樣的方式分配需要有方案進行處理,同時也要考慮,如果該機器當機了,對應的 datacenterId/workerId 分配後的回收問題。

這個問題的解決方案是:每個例項啟動,擴容,直接從 ZK/Redis/DB 等發號器取一個 id 作為 datacenterId/workerId,下線不銷燬;並且維護一個活動節點佇列,在地址空間耗盡的時候,指標指回佇列頭部,當分配的 id 存在於活動節點佇列則跳過取下一個可用空間,達到複用原地址空間的目的。

相關文章