基於SnowFlake演算法如何讓分庫分表中不同的ID落在同一個庫的演算法的實現

笨_鳥_不_會_飛發表於2020-12-01

SnowFlake演算法

​ SnowFlake演算法是由Twitter鎖分享出來的一種生成不重複的分散式ID的一種演算法,在複雜的分散式系統中,我們通常需要使用分庫分表來實現我們的系統,在分庫分表的過程中將會涉及到一個ID重複的問題,資料庫的自增ID很明顯不會滿足要求,此時擁有一個可以生成全域性唯一的ID的演算法是非常有必要的的。

1、生成唯一ID具備的條件

  • 全域性唯一性:在資料庫分庫分表以後,某張表的流水ID必須是唯一的。

  • 趨勢遞增:在MySQL InnoDB引擎中使用的是聚集索引,由於多數RDBMS使用B-tree的資料結構來儲存索引資料,在主鍵的選擇上面我們應該儘量使用有序的主鍵保證寫入效能。

  • 單調遞增:保證下一個ID一定大於上一個ID,如果下個ID有可能小於上一個ID那就有可能導致ID的全域性不唯一性。

  • 資訊保安:如果ID是連續的,惡意使用者的扒取工作就非常容易做了,直接按照順序下載指定URL即可。

2、生成全域性唯一ID的三種方式

2.1、UUID

​ uuid可以保證我們的全域性ID的唯一,如果是在單體的系統使用UUID和遞增ID實際上對效能影響不大,因為畢竟單表的資料量要超過百萬在傳統的單體系統中基本上是很難的,但是在網際網路企業中,單表超過百萬可能就幾個小時的事情,這時候使用UUID在插入資料的時候會比遞增ID效率上差很多。

優點

  • 效能非常高:本地生成,沒有網路消耗

缺點

  • 不易於儲存:UUID太長,16位元組128位,通常以36長度的字串表示,很多場景不適用。
  • 資訊不安全:基於MAC地址生成UUID的演算法可能會造成MAC地址洩露,這個漏洞曾被用於尋找梅麗莎病xxxx者位置。
  • ID作為主鍵時在特定的環境會存在一些問題,比如做DB主鍵的場景下,UUID就非常不適用。(MySQL官方有明確的建議主鍵要儘量越短越好,36個字元長度的UUID不符合要求。對MySQL索引不利:如果作為資料庫主鍵,在InnoDB引擎下,UUID的無序性可能會引起資料位置頻繁變動,嚴重影響效能。)

2.2、基於分散式鎖的ID

​ 基於分散式鎖的方式的ID,就是採用分散式鎖(redis可實現,java採用關鍵字Sychronization也可以實現)的機制來實現,生成某張表ID的時候,採用分散式鎖直接加鎖,生成流水ID以後直接解鎖即可,然後業務上可以直接使用當前ID來生成想要的資料,這時候生成的ID是唯一的。

優點

  • 效能很好:基於redis的效能可以很快的生成的自己想要的流水ID。

缺點

  • 增加成本:需要專門的運維人員去維護我們的redis資料庫。
  • 網路消耗:在與redis互動的過程中會增加網路的消耗。
  • 複雜度:提高了開發的複雜度,首先開發人員需要引入redis的庫,同時要理解分散式鎖,這樣才可以正確的使用分散式鎖。

2.3、SnowFlake

​ SnowFlake演算法是Twitter設計的一個可以在分散式系統中生成唯一的ID的演算法,它可以滿足Twitter每秒上萬條訊息ID分配的請求,這些訊息ID是唯一的且有大致的遞增順序。

2.3.1、SnowFlake演算法原理

在這裡插入圖片描述

  • 因為最高位是標識位,為1表示為負數,所以最高位不使用。

  • 41bit 儲存時間戳,精確到毫秒。也就是說最大可使用的年限是69年。

  • 10bit 的機器位,能部屬在1024臺機器節點來生成ID。

  • 12bit 的序列號,一毫秒最大生成唯一ID的數量為4096個。

2.3.2、41位時間戳

​ 這個是毫秒級的時間,一般實現上不會儲存當前的時間戳,而是時間戳的差值(當前時間-固定的開始時間),這樣可以使產生的ID從更小值開始;41位的時間戳可以使用69年,(1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69年,例如我們可以設定我們當前的開始時間為2020-12-01這天開始,那麼我們的序列號就可以使用到2079-12-01這一天,這已經完全滿足我們的需求了。

2.3.3、10位機器位

​ 可以同時部署到1024臺機器中,在微服務中我們可以為每一個例項分配一個唯一ID。

2.3.4、12位序列號

​ 可以在一個毫秒內生成4096個ID,4096個ID完全已經滿足百分99以上的場景,若有需要可以自己再去擴充套件。

2.4、雪花演算法變種實現

​ 在我們的資料庫中存在分庫和分表的業務場景,例如訂單表,上面存著訂單ID和使用者ID,這時候我們是根據訂單ID來進行分庫分表,那我們希望我們的使用者ID可以和訂單ID落在同一個庫上,那麼這時候我們該如何生成我們的雪花ID呢?

在這裡插入圖片描述

​ 需要保證我們的使用者ID和訂單ID同時落在一個庫上,那麼就是在生成訂單ID的時候,訂單ID取餘的值必須要等於使用者ID取餘的值,這樣才會使得使用者ID和訂單ID落在同一個庫上,那麼需要訂單ID取餘的值落在同一個庫上,那麼我們可以改造我們原先的12位的序列號,將其分為7位的序列號和5位的取餘值,只要保證我們5位的取餘值就是當前使用者ID取餘的值,就可以保證我們生成的訂單ID和使用者ID會落在通一個庫上。

2.4.1、java程式碼實現

/**
 * @author linzef
 * @since 2020-11-29
 * 類描述: 生成雪花ID的工具類
 */
public class SnowflakeIdWorker {

    /**
     * 建構函式
     *
     * @param modCoefficient 當前取餘的係數,例如你設定為32,則生成的id % 32 則會等於你當前設定的modVal
     * @param modVal         當前生成的需要取餘的值,假設你希望生成的雪花ID需要取餘為8,則設定為8,這個值必須要小於31,且要小於modCoefficient的值
     * @param workerId       工作ID (0~31)
     * @param dataCenterId   資料中心ID (0~31)
     */
    public SnowflakeIdWorker(int modCoefficient, int modVal, long twepoch, long workerId, long dataCenterId) {
        this.modCoefficient = modCoefficient;
        this.modVal = modVal;
        this.twepoch = twepoch;
        if (modVal > modCoefficient || modCoefficient < 0) {
            throw new IllegalArgumentException(String.format("modVal的值不能大於modCoefficient取餘的係數的值,或者modCoefficient的值不能小於0", modCoefficient));
        }
        if (workerId > maxWorkerId || workerId < 0) {
            throw new IllegalArgumentException(String.format("workerId不能大於" + maxWorkerId + "的值或者小於0", maxWorkerId));
        }
        if (dataCenterId > maxDataCenterId || dataCenterId < 0) {
            throw new IllegalArgumentException(String.format("dataCenterId不能大於" + maxDataCenterId + "的值或者小於0", maxDataCenterId));
        }
        this.workerId = workerId;
        this.dataCenterId = dataCenterId;
    }

    /**
     * 當前取餘的係數,例如你設定為32,則生成的id % 32 則會等於你當前設定的modVal
     */
    private final int modCoefficient;

    /**
     * 當前生成的需要取餘的值,假設你希望生成的雪花ID需要取餘為8,則設定為8,這個值必須要小於31,且要小於modCoefficient的值
     */
    private final int modVal;

    /**
     * 開始時間截
     */
    private final long twepoch;

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

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

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

    /**
     * 支援的最大資料標識id,結果是31
     */
    private final long maxDataCenterId = -1L ^ (-1L << dataCenterIdBits);

    /**
     * 取餘的值所佔用的位數
     */
    private final long modBits = 5L;

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

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

    /**
     * 資料標識id向左移17位(7+5+5)
     */
    private final long dataCenterIdShift = sequenceBits + workerIdBits + modBits;

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

    /**
     * 生成序列的掩碼,這裡為128(2^7)
     */
    private final long sequenceMask = -1L ^ (-1L << sequenceBits);

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

    /**
     * 資料中心ID(0~31)
     */
    private long dataCenterId;

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

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


    /**
     * 獲得下一個ID (該方法是執行緒安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        long timestamp = timeGen();

        //如果當前時間小於上一次ID生成的時間戳,說明系統時鐘回退過這個時候應當丟擲異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(String.format("時鐘異常,請重試生成雪花ID", lastTimestamp - timestamp));
        }

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

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

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

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

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


    /**
     * 測試
     */
    public static void main(String[] args) {
        /**
         * 取餘的係數設定為16,取餘的值設定為13,這樣你會發現生成的雪花ID的值被16取餘以後為13
         */
        SnowflakeIdWorker idWorker = new SnowflakeIdWorker(16, 13, 28778794000L, 0, 0);
        for (int i = 0; i < 100; i++) {
            long id = idWorker.nextId();
            System.out.println((Long.toBinaryString(id)));
            System.out.println(id);
            System.out.println(id % 16);
        }
    }

}

相關文章