分佈式惟一ID生成系列(5)——Twitter的雪法算法Snowflake適合作分佈式ID嗎

寫到這裏,分佈式Id算是寫到最後一篇了,在這一篇裏,我會講到目前網上最適合分佈式Id的方法,什麼方法呢,請您往下看:html

介紹Snowflake算法

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

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

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

這裏我麼來說一下這個結構:首先由於window是64位的,而後整數的時候第一位必須是0,因此最大的數值就是63位的111111111111111111111111111111111111111111111111111111111111111,而後呢Snowflake算法分出來41位做爲毫秒值,而後10位做爲redis節點的數量,而後12位作成redis節點在每一毫秒的自增序列值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值segmentfault

因爲在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的建立了less

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/art...
https://www.liaoxuefeng.com/a...
https://tech.meituan.com/2017...
https://segmentfault.com/a/11...
https://www.jianshu.com/p/9d7...

相關文章
相關標籤/搜索