開發與維運

什麼是雪花ID?

文章已收錄Github精選,歡迎Starhttps://github.com/yehongzhi/learningSummary

為什麼使用雪花ID

在以前的項目中,最常見的兩種主鍵類型是自增Id和UUID,在比較這兩種ID之前首先要搞明白一個問題,就是為什麼主鍵有序比無序查詢效率要快,因為自增Id和UUID之間最大的不同點就在於有序性。

我們都知道,當我們定義了主鍵時,數據庫會選擇表的主鍵作為聚集索引(B+Tree),mysql 在底層是以數據頁為單位來存儲數據的。

也就是說如果主鍵為自增 id 的話,mysql 在寫滿一個數據頁的時候,直接申請另一個新數據頁接著寫就可以了。如果一個數據頁存滿了,mysql 就會去申請一個新的數據頁來存儲數據。如果主鍵是UUID,為了確保索引有序,mysql 就需要將每次插入的數據都放到合適的位置上。這就造成了頁分裂,這個大量移動數據的過程是會嚴重影響插入效率的

一句話總結就是,InnoDB表的數據寫入順序能和B+樹索引的葉子節點順序一致的話,這時候存取效率是最高的。

但是為什麼很多情況又不用自增id作為主鍵呢?

  • 容易導致主鍵重複。比如導入舊數據時,線上又有新的數據新增,這時就有可能在導入時發生主鍵重複的異常。為了避免導入數據時出現主鍵重複的情況,要選擇在應用停業後導入舊數據,導入完成後再啟動應用。顯然這樣會造成不必要的麻煩。而UUID作為主鍵就不用擔心這種情況。
  • 不利於數據庫的擴展。當採用自增id時,分庫分表也會有主鍵重複的問題。UUID則不用擔心這種問題。

那麼問題就來了,自增id會擔心主鍵重複,UUID不能保證有序性,有沒有一種ID既是有序的,又是唯一的呢?

當然有,就是雪花ID

什麼是雪花ID

snowflake是Twitter開源的分佈式ID生成算法,結果是64bit的Long類型的ID,有著全局唯一和有序遞增的特點。

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

缺點也是有的,就是強依賴機器時鐘,如果機器上時鐘回撥,有可能會導致主鍵重複的問題。

Java實現雪花ID

下面是用Java實現雪花ID的代碼,供大家參考一下。

public class SnowflakeIdWorker {
    /**
     * 開始時間:2020-01-01 00:00:00
     */
    private final long beginTs = 1577808000000L;

    private final long workerIdBits = 10;

    /**
     * 2^10 - 1 = 1023
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);

    private final long sequenceBits = 12;

    /**
     * 2^12 - 1 = 4095
     */
    private final long maxSequence = -1L ^ (-1L << sequenceBits);

    /**
     * 時間戳左移22位
     */
    private final long timestampLeftOffset = workerIdBits + sequenceBits;

    /**
     * 業務ID左移12位
     */
    private final long workerIdLeftOffset = sequenceBits;

    /**
     * 合併了機器ID和數據標示ID,統稱業務ID,10位
     */
    private long workerId;

    /**
     * 毫秒內序列,12位,2^12 = 4096個數字
     */
    private long sequence = 0L;

    /**
     * 上一次生成的ID的時間戳,同一個worker中
     */
    private long lastTimestamp = -1L;

    public SnowflakeIdWorker(long workerId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("WorkerId必須大於或等於0且小於或等於%d", maxWorkerId));
        }

        this.workerId = workerId;
    }

    public synchronized long nextId() {
        long ts = System.currentTimeMillis();
        if (ts < lastTimestamp) {
            throw new RuntimeException(String.format("系統時鐘回退了%d毫秒", (lastTimestamp - ts)));
        }

        // 同一時間內,則計算序列號
        if (ts == lastTimestamp) {
            // 序列號溢出
            if (++sequence > maxSequence) {
                ts = tilNextMillis(lastTimestamp);
                sequence = 0L;
            }
        } else {
            // 時間戳改變,重置序列號
            sequence = 0L;
        }

        lastTimestamp = ts;

        // 0 - 00000000 00000000 00000000 00000000 00000000 0 - 00000000 00 - 00000000 0000
        // 左移後,低位補0,進行按位或運算相當於二進制拼接
        // 本來高位還有個0<<63,0與任何數字按位或都是本身,所以寫不寫效果一樣
        return (ts - beginTs) << timestampLeftOffset | workerId << workerIdLeftOffset | sequence;
    }

    /**
     * 阻塞到下一個毫秒
     *
     * @param lastTimestamp
     * @return
     */
    private long tilNextMillis(long lastTimestamp) {
        long ts = System.currentTimeMillis();
        while (ts <= lastTimestamp) {
            ts = System.currentTimeMillis();
        }

        return ts;
    }

    public static void main(String[] args) {
        SnowflakeIdWorker snowflakeIdWorker = new SnowflakeIdWorker(7);
        for (int i = 0; i < 10; i++) {
            long id = snowflakeIdWorker.nextId();
            System.out.println(id);
        }
    }
}

main方法,測試結果如下:

184309536616640512
184309536616640513
184309536616640514
184309536616640515
184309536616640516
184309536616640517
184309536616640518
184309536616640519
184309536616640520
184309536616640521

總結

在大部分公司的開發項目中裡,雪花ID是主流的ID生成策略,除了自己實現之外,目前市場上也有很多開源的實現,比如:

有興趣的可以自行觀摩一下,那麼這篇文章就寫到這裡了,感謝大家的閱讀。

覺得有用就點個贊吧,你的點贊是我創作的最大動力~

我是一個努力讓大家記住的程序員。我們下期再見!!!
在這裡插入圖片描述

能力有限,如果有什麼錯誤或者不當之處,請大家批評指正,一起學習交流!

Leave a Reply

Your email address will not be published. Required fields are marked *