分散式ID系列(5)——Twitter的雪法演算法Snowflake適合做分散式ID嗎

IT乾坤發表於2019-08-14

介紹Snowflake演算法

SnowFlake演算法是國際大公司Twitter的採用的一種生成分散式自增id的策略,這個演算法產生的分散式id是足夠我們我們中小公司在日常裡面的使用了。我也是比較推薦這一種演算法產生的分散式id的。

演算法snowflake的生成的分散式id結構組成部分

演算法snowflake生成id的結果是一個64bit大小的整數,它的結構如下圖,
分散式ID系列(5)——Twitter的雪法演算法Snowflake適合做分散式ID嗎

這裡我麼來講一下這個結構:首先因為window是64位的,然後整數的時候第一位必須是0,所以最大的數值就是63位的111111111111111111111111111111111111111111111111111111111111111,然後呢Snowflake演算法分出來41位作為毫秒值,然後10位作為redis節點的數量,然後12位做成redis節點在每一毫秒的自增序列值

41位的二進位制11111111111111111111111111111111111111111轉換成10進位制的毫秒就是2199023255551,然後我們把 2199023255551轉換成時間就是2039-09-07,也就是說可以用20年的(這裡在網上會有很多說是可以使用69年的,他們說69年的也對,因為1970年+69年的結果就是2039年,但是如果從今年2019年來說,也就只能用20年了)

然後10位作為節點,所以最多就是12位的1111111111,也就是最多可以支援1023個節點,

然後10位表示每一個節點自增序列值,這裡最多就是10位的111111111111,也就是說每一個節點可以每一毫秒可以最多生成4059個不重複id值

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

Java實現Snowflake演算法的原始碼

Snowflake演算法的原始碼如下所示(這個是我從網上找到的),這裡我進行了測試了一波,結果如下所示

package com.hello;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {
    /**
     * 開始時間截 (1970-01-01)
     */
    private final long twepoch = 0L;

    /**
     * 機器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);

    /**
     * 序列在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 ^ (-1L << sequenceBits);

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

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

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

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

    public Test(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;
    }

    /**
     * 獲得下一個ID (該方法是執行緒安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        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 當前時間戳
     */
    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 parseId(long id) {
        long miliSecond = id >>> 22;
        long shardId = (id & (0xFFF << 10)) >> 10;
        System.err.println("分散式id-"+id+"生成的時間是:"+new SimpleDateFormat("yyyy-MM-dd").format(new Date(miliSecond)));
    }

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

執行結果如下所示,此時我們可以看到,不僅可以可以把分散式id給建立處理,而且可以把這個建立的時間也列印出來,此時就可以滿足我們的分散式id的建立了

6566884785623400448
分散式id-6566884785623400448生成的時間是:2019-08-13
6566884785812144128
分散式id-6566884785812144128生成的時間是:2019-08-13
6566884785812144129
分散式id-6566884785812144129生成的時間是:2019-08-13
6566884785812144130
分散式id-6566884785812144130生成的時間是:2019-08-13
6566884785812144131
分散式id-6566884785812144131生成的時間是:2019-08-13
6566884785812144132
分散式id-6566884785812144132生成的時間是:2019-08-13
6566884785816338432
分散式id-6566884785816338432生成的時間是:2019-08-13
6566884785816338433
分散式id-6566884785816338433生成的時間是:2019-08-13
6566884785816338434
分散式id-6566884785816338434生成的時間是:2019-08-13
6566884785816338435
分散式id-6566884785816338435生成的時間是:2019-08-13

縮小版Snowflake演算法生成分散式id

因為Snowflake演算法的極限是每毫秒的每一個節點生成4059個id值,也就是說每毫秒的極限是生成023*4059=4 152 357個id值,這樣生成id值的速度對於twitter公司來說是很符合標準的(畢竟人家公司大嘛),但是對於我們們中小公司來說是不需要的,所以我們可以根據Snowflake演算法來修改一下分散式id的建立,讓每秒建立的id少一些,但是把可以使用的時間擴大一些

這裡我看廖雪峰老師的文章之後,採用了53位作為分散式id值的位數,因為如果後端和前端的JavaScript打交道的話,由於JavaScript支援的最大整型就是53位,超過這個位數,JavaScript將丟失精度。因此,使用53位整數可以直接由JavaScript讀取,而超過53位時,就必須轉換成字串才能保證JavaScript處理正確,所以我們的分散式id就用53位來生成

這53位裡面,第一位還是0,然後剩下的52位,33位作為秒數,4位作為節點數,15位作為每一個節點在每一秒的生成序列值

33位的二進位制111111111111111111111111111111111轉換成10進位制的秒就是8589934591,然後我們把 8589934591轉換成時間就是2242-03-16,也就是說可以用220年的,足夠我們的使用了

然後4位節點,所以最多就是4位的1111,也就是最多可以支援15個節點,

然後15位表示每一個節點每一秒自增序列值,這裡最多就是10位的11111111111111111,也就是說每一個節點可以每一秒可以最多生成131071個不重複id值

這樣算起來,就是說每一秒每一個節點生成131071個不重複的節點,所以極限就是每秒生成15*131071=1 966 065個分散式id,夠我們在開發裡面的日常使用了

所以程式碼就可以變成下面這樣,這裡主要講一下下面的nextId()方法,
首先藍色程式碼是獲取當前秒,然後進行校驗,就是把當前時間和上一個時間戳進行比較,如果當前時間比上一個時間戳要小,那就說明系統時鐘回退,所以此時應該丟擲異常
然後是下面的紅色程式碼,首先如果是同一秒生成的,那麼就把這一秒的生成序列id值一直增加,一直增加到131071個,如果在增加,那麼下面的紅色程式碼裡面的sequence = (sequence + 1) & sequenceMask;的值就會是0,那麼就會執行紅色程式碼裡面的tilNextMillis()方法進行阻塞,直到獲取到下一秒繼續執行
然後下面的綠色程式碼表示每一秒過去之後,都要把這個生成序列的id值都變成0,這樣在新的一秒裡面就可以在繼續生成1到131071個分散式id值了
然後下面的黃色程式碼就是把我們們的秒,節點值,節點每秒生成序列id值加起來組成一個分散式id返回

package com.hello;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test {

    /**
     * 開始時間截 (1970-01-01)
     */
    private final long twepoch = 0L;

    /**
     * 機器id,範圍是1到15
     */
    private final long workerId;

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

    /**
     * 支援的最大機器id,結果是15
     */
    private final long maxWorkerId = ~(-1L << workerIdBits);

    /**
     * 生成序列佔的位數
     */
    private final long sequenceBits = 15L;

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

    /**
     * 生成序列的掩碼,這裡為最大是32767 (1111111111111=32767)
     */
    private final long sequenceMask = ~(-1L << sequenceBits);

    /**
     * 時間截向左移19位(4+15)
     */
    private final long timestampLeftShift = 19L;


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

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


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

    /**
     * 獲得下一個ID (該方法是執行緒安全的)
     *
     * @return SnowflakeId
     */
    public synchronized long nextId() {
        //藍色程式碼註釋開始
        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;
        //黃色程式碼註釋開始
        //移位並通過或運算拼到一起組成53 位的ID
        return ((timestamp - twepoch) << timestampLeftShift)
            | (workerId << workerIdShift)
            | sequence;
        //黃色程式碼註釋結束
    }

    /**
     * 阻塞到下一個秒,直到獲得新的時間戳
     *
     * @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()/1000L;
    }

    public static void parseId(long id) {
        long second = id >>> 19;
        System.err.println("分散式id-"+id+"生成的時間是:"+new SimpleDateFormat("yyyy-MM-dd").format(new Date(second*1000)));
    }

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

此時結果如下所示,都是ok的,؏؏☝ᖗ乛◡乛ᖘ☝؏؏

820870564020224
分散式id-820870564020224生成的時間是:2019-08-13
820870564020225
分散式id-820870564020225生成的時間是:2019-08-13
820870564020226
分散式id-820870564020226生成的時間是:2019-08-13
820870564020227
分散式id-820870564020227生成的時間是:2019-08-13
820870564020228
分散式id-820870564020228生成的時間是:2019-08-13
820870564020229
分散式id-820870564020229生成的時間是:2019-08-13
820870564020230
分散式id-820870564020230生成的時間是:2019-08-13
820870564020231
分散式id-820870564020231生成的時間是:2019-08-13
820870564020232
分散式id-820870564020232生成的時間是:2019-08-13
820870564020233
分散式id-820870564020233生成的時間是:2019-08-13

雪法演算法Snowflake適合做分散式ID嗎

根據一系列的分散式id講解,雪法演算法Snowflake是目前網上最適合做分散式Id的了,大家如果想用的話,可以根據我上面的縮小版的Snowflake演算法來作為我們開發中的使用。؏؏☝ᖗ乛◡乛ᖘ☝؏؏

原文連結

其他分散式ID系列快捷鍵:
分散式ID系列(1)——為什麼需要分散式ID以及分散式ID的業務需求
分散式ID系列(2)——UUID適合做分散式ID嗎
分散式ID系列(3)——資料庫自增ID機制適合做分散式ID嗎
分散式ID系列(4)——Redis叢集實現的分散式ID適合做分散式ID嗎
分散式ID系列(5)——Twitter的雪法演算法Snowflake適合做分散式ID嗎

大佬網址
https://www.itqiankun.com/article/1565747019
https://www.liaoxuefeng.com/article/1280526512029729
https://tech.meituan.com/2017/04/21/mt-leaf.html
https://segmentfault.com/a/1190000011282426
https://www.jianshu.com/p/9d7ebe37215e
https://segmentfault.com/a/1190000011282426

相關文章