雪花算法分析與實現

雪花生成過程

https://baike.baidu.com/item/%E9%9B%AA%E8%8A%B1/8012054?fr=aladdin

在冰晶增長的同時,冰晶附近的水汽會被消耗。所以,越靠近冰晶的地方,水汽越稀薄,過飽和程度越低。在緊靠冰晶表面的地方,因爲多餘的水汽都已凝華在冰晶上了,所以剛剛達到飽和。這樣,靠近冰晶處的水汽密度就要比離它遠的地方小。水汽就從冰晶周圍向冰晶所在處移動。水汽分子首先遇到冰晶的各個角棱和凸出部分,並在這裏凝華而使冰晶增長。於是冰晶的各個角棱和凸出部分將首先迅速地增長,而逐漸成爲枝叉狀。以後,又因爲同樣的原因在各個枝叉和角棱處長出新的小枝叉來。與此同時,在各個角棱和枝叉之間的凹陷處。空氣已經不再是飽和的了。有時,在這裏甚至有昇華過程,以致水汽被輸送到其他地方去。這樣就使得角棱和枝叉更爲突出,而慢慢地形成了我們熟悉的星狀雪花。
在這裏插入圖片描述

分佈式中ID的常用解決方案

https://blog.csdn.net/m0_37041378/article/details/78125747

在複雜的系統中,往往需要對大量的數據如訂單,賬戶進行標識,以一個有意義的有序的序列號來作爲全局唯一的ID;
而分佈式系統中我們對ID生成器要求又有哪些呢?

全局唯一性

不能出現重複的ID號,既然是唯一標識,這是最基本的要求。

遞增

比較低要求的條件爲趨勢遞增,即保證下一個ID一定大於上一個ID,而比較苛刻的要求是連續遞增,如1,2,3等等。

高可用高性能

ID生成事關重大,一旦掛掉系統崩潰;高性能是指必須要在壓測下表現良好,如果達不到要求則在高併發環境下依然會導致系統癱瘓。

信息安全

如果ID是連續的,惡意用戶的扒取工作就非常容易做了,直接按照順序下載指定URL即可;如果是訂單號就更危險了,競對可以直接知道我們一天的單量。所以在一些應用場景下,會需要ID無規則、不規則。

第二條和第四條有點衝突,需要結合具體的業務場景。

常見企業級解決方案

UUID

優點:

能夠保證獨立性,程序可以在不同的數據庫間遷移,效果不受影響。

保證生成的ID不僅是表獨立的,而且是庫獨立的,這點在你想切分數據庫的時候尤爲重要。

缺點:

  1. 性能爲題:UUID太長,通常以36長度的字符串表示,對MySQL索引不利:如果作爲數據庫主鍵,在InnoDB引擎下,UUID的無序性可能會引起數據位置頻繁變動,嚴重影響性能

  2. UUID無業務含義:很多需要ID能標識業務含義的地方不使用

  3. 不滿足遞增要求

  4. 信息不安全:基於MAC地址生成UUID的算法可能會造成MAC地址泄露,這個漏洞曾被用於尋找梅麗莎病毒的製作者位置。

  5. 不易於存儲:UUID太長,16字節128位,通常以36長度的字符串表示,很多場景不適用。

基於數據庫方案

利用數據庫生成ID是最常見的方案。能夠確保ID全數據庫唯一。其優缺點如下:

優點:

  1. 非常簡單,利用現有數據庫系統的功能實現,成本小,有DBA專業維護。
  2. ID號單調自增,可以實現一些對ID有特殊要求的業務。

缺點:

不同數據庫語法和實現不同,數據庫遷移的時候或多數據庫版本支持的時候需要處理。

  1. 在單個數據庫或讀寫分離或一主多從的情況下,只有一個主庫可以生成。
  2. 有單點故障的風險。 在性能達不到要求的情況下,比較難於擴展。
  3. 如果涉及多個系統需要合併或者數據遷移會比較麻煩。
  4. 分表分庫的時候會有麻煩。

雪花算法

在這裏插入圖片描述

  • 41位爲時間戳,12位爲在這一刻能夠產生2^12個自增的Id
  • 這結合了自增Id的優勢,同時10位機器ID(dataCenterId 5位和machineId 5位)確保了分佈式能夠支持1024臺節點

缺點:

  • 強依賴時鐘,如果主機時間回撥,則會造成重複ID

Java實現


public class SnowFlake {
    /**
     * 開始時間截 (2015-01-01)
     */
    private final long twepoch = 1420041600000L;

    /**
     * 機器id所佔的位數
     */
    private final long workerIdBits = 5L;

    /**
     * 數據標識id所佔的位數
     */
    private final long dataCenterIdBits = 5L;

    /**
     * 支持的最大機器id,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
     */
    private final long maxWorkerId = ~(-1L << workerIdBits);

    /**
     * 支持的最大數據標識id,結果是31
     */
    private final long maxDataCenterId = ~(-1L << dataCenterIdBits);

    /**
     * 序列在id中佔的位數
     */
    private final long sequenceBits = 12L;

    /**
     * 機器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;

    /**
     * 數據標識id向左移17位(12+5)
     */
    private final long dataCenterIdShift = sequenceBits + workerIdBits;

    /**
     * 時間截向左移22位(5+5+12)
     */
    private final long timestampLeftShift = sequenceBits + workerIdBits + dataCenterIdBits;

    /**
     *
     * 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
     */
    private final long sequenceMask = ~(-1L << sequenceBits);

    /**
     * 工作機器ID(0~31)
     */
    private volatile long workerId;

    /**
     * 數據中心ID(0~31)
     */
    private volatile long dataCenterId;

    /**
     * 毫秒內序列(0~4095)
     */
    private volatile long sequence = 0L;

    /**
     * 上次生成ID的時間截
     */
    private volatile long lastTimestamp = -1L;

    //==============================Constructors=====================================

    /**
     * 構造函數
     *
     * @param workerId     工作ID (0~31)
     * @param dataCenterId 數據中心ID (0~31)
     */

    public SnowFlake(long workerId, long dataCenterId) {
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("dataCenter Id can't be greater than %d or less than 0", maxDataCenterId));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    // ==============================Methods==========================================

    /**
     * 獲得下一個ID (該方法是線程安全的)
     *  如果一個線程反覆獲取Synchronized鎖,那麼synchronized鎖將變成偏向鎖。
     * @return SnowflakeId
     */
    public synchronized long nextId() throws RuntimeException {
        long timestamp = timeGen();

        //如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException((String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)));

        }

        //如果是同一時間生成的,則進行毫秒內序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內序列溢出
            if (sequence == 0) {
                //阻塞到下一個毫秒,獲得新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時間戳改變,毫秒內序列重置
        else {
            sequence = 0L;
        }

        //上次生成ID的時間截
        lastTimestamp = timestamp;

        //移位並通過或運算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift)
                | (dataCenterId << dataCenterIdShift)
                | (workerId << workerIdShift)
                | sequence;
    }

    /**
     * 阻塞到下一個毫秒,直到獲得新的時間戳
     *
     * @param lastTimestamp 上次生成ID的時間截
     * @return 當前時間戳
     */
    private long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }

    /**
     * 返回以毫秒爲單位的當前時間
     *
     * @return 當前時間(毫秒)
     */
    private long timeGen() {
        return System.currentTimeMillis();
    }


}

爲什麼叫雪花算法呢?私以爲衆所周知世界上沒有一對相同的雪花,而雪花形成的過程中複雜的環境條件則對應了雪花算法中的機器ID,時間戳在現實層面上能夠精確到普朗克時間10^-43s,這樣時間戳的長度也得以保證。

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