分布式唯一ID系列(5)——Twitter的雪法算法Snowflake適合做分布式ID嗎


介紹Snowflake算法

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

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

算法snowflake生成id的結果是一個64bit大小的整數,它的結構如下圖,

這里我么來講一下這個結構:首先因為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

時間回撥問題的解決

這里找了好多,終於找出來兩個方法:想看的點擊這個鏈接吧:分布式ID系列(5)——Twitter的雪法算法Snowflake適合做分布式ID嗎

雪法算法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嗎


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM