(5)分布式ID之雪法算法Snowflake實現(xiàn)分布式ID

介紹Snowflake算法

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

算法snowflake的生成的分布式id結(jié)構(gòu)組成部分

算法snowflake生成id的結(jié)果是一個64bit大小的整數(shù)案铺,它的結(jié)構(gòu)如下圖,

image

這里我么來講一下這個結(jié)構(gòu):首先因為window是64位的,然后整數(shù)的時候第一位必須是0货抄,所以最大的數(shù)值就是63位的111111111111111111111111111111111111111111111111111111111111111,然后呢Snowflake算法分出來41位作為毫秒值,然后10位作為redis節(jié)點的數(shù)量碉熄,然后12位做成redis節(jié)點在每一毫秒的自增序列值

41位的二進制11111111111111111111111111111111111111111轉(zhuǎn)換成10進制的毫秒就是2199023255551桨武,然后我們把 2199023255551轉(zhuǎn)換成時間就是2039-09-07,也就是說可以用20年的(這里在網(wǎng)上會有很多說是可以使用69年的锈津,他們說69年的也對呀酸,因為1970年+69年的結(jié)果就是2039年击喂,但是如果從今年2019年來說擅笔,也就只能用20年了)

然后10位作為節(jié)點,所以最多就是12位的1111111111杨蛋,也就是最多可以支持1023個節(jié)點茎杂,

然后10位表示每一個節(jié)點自增序列值错览,這里最多就是10位的111111111111,也就是說每一個節(jié)點可以每一毫秒可以最多生成4059個不重復(fù)id值

由于在Java中64bit的整數(shù)是long類型煌往,所以在Java中SnowFlake算法生成的id就是long來存儲的倾哺。

Java實現(xiàn)Snowflake算法的源碼

Snowflake算法的源碼如下所示(這個是我從網(wǎng)上找到的),這里我進行了測試了一波刽脖,結(jié)果如下所示

package com.hello;
import java.text.SimpleDateFormat;
import java.util.Date;
public class Test {
    /**
     * 開始時間截 (1970-01-01)
     */
    private final long twepoch = 0L;
    /**
     * 機器id所占的位數(shù)
     */
    private final long workerIdBits = 5L;
    /**
     * 數(shù)據(jù)標(biāo)識id所占的位數(shù)
     */
    private final long datacenterIdBits = 5L;
    /**
     * 支持的最大機器id羞海,結(jié)果是31 (這個移位算法可以很快的計算出幾位二進制數(shù)所能表示的最大十進制數(shù))
     */
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    /**
     * 支持的最大數(shù)據(jù)標(biāo)識id,結(jié)果是31
     */
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    /**
     * 序列在id中占的位數(shù)
     */
    private final long sequenceBits = 12L;
    /**
     * 機器ID向左移12位
     */
    private final long workerIdShift = sequenceBits;
    /**
     * 數(shù)據(jù)標(biāo)識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;
    /**
     * 數(shù)據(jù)中心ID(0~31)
     */
    private long datacenterId;
    /**
     * 毫秒內(nèi)序列(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();
        //如果當(dāng)前時間小于上一次ID生成的時間戳却邓,說明系統(tǒng)時鐘回退過這個時候應(yīng)當(dāng)拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
        //如果是同一時間生成的,則進行毫秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //毫秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個毫秒,獲得新的時間戳
                timestamp = tilNextMillis(lastTimestamp);
            }
        }
        //時間戳改變院水,毫秒內(nèi)序列重置
        else {
            sequence = 0L;
        }
        //上次生成ID的時間截
        lastTimestamp = timestamp;
        //移位并通過或運算拼到一起組成64位的ID
        return ((timestamp - twepoch) << timestampLeftShift) //
            | (datacenterId << datacenterIdShift) //
            | (workerId << workerIdShift) //
            | sequence;
    }
    /**
     * 阻塞到下一個毫秒腊徙,直到獲得新的時間戳
     *
     * @param lastTimestamp 上次生成ID的時間截
     * @return 當(dāng)前時間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    /**
     * 返回以毫秒為單位的當(dāng)前時間
     *
     * @return 當(dāng)前時間(毫秒)
     */
    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);
        }
    }
}

執(zhí)行結(jié)果如下所示,此時我們可以看到檬某,不僅可以可以把分布式id給創(chuàng)建處理撬腾,而且可以把這個創(chuàng)建的時間也打印出來,此時就可以滿足我們的分布式id的創(chuàng)建了

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算法的極限是每毫秒的每一個節(jié)點生成4059個id值恢恼,也就是說每毫秒的極限是生成023*4059=4 152 357個id值民傻,這樣生成id值的速度對于twitter公司來說是很符合標(biāo)準(zhǔn)的(畢竟人家公司大嘛),但是對于咱們中小公司來說是不需要的厅瞎,所以我們可以根據(jù)Snowflake算法來修改一下分布式id的創(chuàng)建饰潜,讓每秒創(chuàng)建的id少一些,但是把可以使用的時間擴大一些

這里我看廖雪峰老師的文章之后和簸,采用了53位作為分布式id值的位數(shù)彭雾,因為如果后端和前端的JavaScript打交道的話,由于JavaScript支持的最大整型就是53位锁保,超過這個位數(shù)薯酝,JavaScript將丟失精度半沽。因此,使用53位整數(shù)可以直接由JavaScript讀取吴菠,而超過53位時者填,就必須轉(zhuǎn)換成字符串才能保證JavaScript處理正確,所以我們的分布式id就用53位來生成

這53位里面做葵,第一位還是0占哟,然后剩下的52位,33位作為秒數(shù)酿矢,4位作為節(jié)點數(shù)榨乎,15位作為每一個節(jié)點在每一秒的生成序列值

33位的二進制111111111111111111111111111111111轉(zhuǎn)換成10進制的秒就是8589934591,然后我們把 8589934591轉(zhuǎn)換成時間就是2242-03-16瘫筐,也就是說可以用220年的蜜暑,足夠我們的使用了

然后4位節(jié)點,所以最多就是4位的1111策肝,也就是最多可以支持15個節(jié)點肛捍,

然后15位表示每一個節(jié)點每一秒自增序列值,這里最多就是10位的11111111111111111之众,也就是說每一個節(jié)點可以每一秒可以最多生成131071個不重復(fù)id值

這樣算起來拙毫,就是說每一秒每一個節(jié)點生成131071個不重復(fù)的節(jié)點,所以極限就是每秒生成15*131071=1 966 065個分布式id酝枢,夠我們在開發(fā)里面的日常使用了

所以代碼就可以變成下面這樣恬偷,這里主要講一下下面的nextId()方法悍手,
首先藍色代碼是獲取當(dāng)前秒帘睦,然后進行校驗,就是把當(dāng)前時間和上一個時間戳進行比較坦康,如果當(dāng)前時間比上一個時間戳要小竣付,那就說明系統(tǒng)時鐘回退,所以此時應(yīng)該拋出異常
然后是下面的紅色代碼滞欠,首先如果是同一秒生成的古胆,那么就把這一秒的生成序列id值一直增加,一直增加到131071個筛璧,如果在增加逸绎,那么下面的紅色代碼里面的sequence = (sequence + 1) & sequenceMask;的值就會是0,那么就會執(zhí)行紅色代碼里面的tilNextMillis()方法進行阻塞夭谤,直到獲取到下一秒繼續(xù)執(zhí)行
然后下面的綠色代碼表示每一秒過去之后棺牧,都要把這個生成序列的id值都變成0,這樣在新的一秒里面就可以在繼續(xù)生成1到131071個分布式id值了
然后下面的黃色代碼就是把咱們的秒朗儒,節(jié)點值颊乘,節(jié)點每秒生成序列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所占的位數(shù),占4位
     */
    private final long workerIdBits = 4L;
    /**
     * 支持的最大機器id乏悄,結(jié)果是15
     */
    private final long maxWorkerId = ~(-1L << workerIdBits);
    /**
     * 生成序列占的位數(shù)
     */
    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;
    /**
     * 秒內(nèi)序列(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();
        //如果當(dāng)前時間小于上一次ID生成的時間戳,說明系統(tǒng)時鐘回退過這個時候應(yīng)當(dāng)拋出異常
        if (timestamp < lastTimestamp) {
            throw new RuntimeException(
                String.format("Clock moved backwards.  Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
        }
         //藍色代碼注釋結(jié)束
         //紅色代碼注釋開始
        //如果是同一時間生成的檩小,則進行秒內(nèi)序列
        if (lastTimestamp == timestamp) {
            sequence = (sequence + 1) & sequenceMask;
            //秒內(nèi)序列溢出
            if (sequence == 0) {
                //阻塞到下一個秒,獲得新的秒值
                timestamp = tilNextMillis(lastTimestamp);
            }
        //時間戳改變开呐,秒內(nèi)序列重置
        }
        //紅色代碼注釋結(jié)束
        //綠色代碼注釋開始
        else {
            sequence = 0L;
        }
        //綠色代碼注釋結(jié)束
        //上次生成ID的時間截
        lastTimestamp = timestamp;
        //黃色代碼注釋開始
        //移位并通過或運算拼到一起組成53 位的ID
        return ((timestamp - twepoch) << timestampLeftShift)
            | (workerId << workerIdShift)
            | sequence;
        //黃色代碼注釋結(jié)束
    }
    /**
     * 阻塞到下一個秒,直到獲得新的時間戳
     *
     * @param lastTimestamp 上次生成ID的時間截
     * @return 當(dāng)前時間戳
     */
    protected long tilNextMillis(long lastTimestamp) {
        long timestamp = timeGen();
        while (timestamp <= lastTimestamp) {
            timestamp = timeGen();
        }
        return timestamp;
    }
    /**
     * 返回以秒為單位的當(dāng)前時間
     *
     * @return 當(dāng)前時間(秒)
     */
    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);
        }
    }
}

此時結(jié)果如下所示规求,都是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
時間回撥問題的解決
  • 第一種方式

可以通過創(chuàng)建一個數(shù)組的方式,比如說200長度的數(shù)組颓哮,這個數(shù)組中存儲上一次該位置對應(yīng)的毫秒數(shù)的生成的唯一id

就比如111毫秒和311毫秒吧家妆,那么就把111毫秒生成的id值和311毫秒生成的id值放到索引位置的第111位

然后當(dāng)發(fā)生時間回撥的時候,假設(shè)時間是第523毫秒冕茅,這可能是最新時間伤极,也可能是從某個時間回撥回來的,不去care姨伤,只看第523毫秒有沒有生成過id哨坪,怎么看?我們?nèi)タ?23號索引位置處查看值乍楚,有三種可能的情況:
a.123號索引沒值——用523毫秒生成新id当编,肯定不重復(fù);
b.123號索引有值徒溪、記錄的時間是323毫秒——用523毫秒生成新id忿偷,肯定不重復(fù);
c.123號索引里有值臊泌、記錄的時間是723毫秒——把此時723毫秒對應(yīng)的記錄的id+1當(dāng)成523毫秒的id值鲤桥,肯定不重復(fù)
c情況里面思考一個問題:就比如523毫秒吧,如果此時是c情況渠概,那么此時523毫秒生成的id值就是(723毫秒生成的id+1)茶凳,怎么確保(723毫秒生成的id+1)這個id值不會被724毫秒,或者725毫秒生成呢播揪,答案是很小幾率重復(fù)的:因為時間戳是id的組成部分贮喧,所以如果毫秒數(shù)不同,id不會重復(fù)的猪狈。但是在極端情況下也是有可能的箱沦,如下:當(dāng)123號索引里的值恰好是第723毫秒所有可能id中的最大值(即:后22bit全是1),對這個值+1罪裹,由于進位饱普,會導(dǎo)致時間戳部分變成724毫秒
具體的思路可以看下面的代碼:http://www.reibang.com/p/b1124283fc43

  • 第二種方式:可以利用擴展位

因為Snowflake算法的極限是每毫秒的每一個節(jié)點生成4059個id值运挫,也就是說每毫秒的極限是生成023*4059=4 152 357個id值,這對于中小公司來說根本就是浪費套耕,所以我們可以把這里的位數(shù)給減少一些谁帕,就比如把12位的序列號變成10位,留兩位給時間回撥冯袍,這樣就很容易的解決了時間回撥問題匈挖,如果發(fā)生時間回撥,那么就直接給當(dāng)前id加1

雪法算法Snowflake適合做分布式ID嗎

根據(jù)一系列的分布式id講解康愤,雪法算法Snowflake是目前網(wǎng)上最適合做分布式Id的了儡循,大家如果想用的話,可以根據(jù)我上面的縮小版的Snowflake算法來作為我們開發(fā)中的使用征冷。????乛?乛????

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末择膝,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子检激,更是在濱河造成了極大的恐慌肴捉,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件叔收,死亡現(xiàn)場離奇詭異齿穗,居然都是意外死亡,警方通過查閱死者的電腦和手機饺律,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進店門窃页,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人复濒,你說我怎么就攤上這事脖卖。” “怎么了芝薇?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵胚嘲,是天一觀的道長作儿。 經(jīng)常有香客問我洛二,道長,這世上最難降的妖魔是什么攻锰? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任晾嘶,我火速辦了婚禮,結(jié)果婚禮上娶吞,老公的妹妹穿的比我還像新娘垒迂。我一直安慰自己,他們只是感情好妒蛇,可當(dāng)我...
    茶點故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布机断。 她就那樣靜靜地躺著楷拳,像睡著了一般。 火紅的嫁衣襯著肌膚如雪吏奸。 梳的紋絲不亂的頭發(fā)上欢揖,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天,我揣著相機與錄音奋蔚,去河邊找鬼她混。 笑死,一個胖子當(dāng)著我的面吹牛泊碑,可吹牛的內(nèi)容都是我干的坤按。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼馒过,長吁一口氣:“原來是場噩夢啊……” “哼臭脓!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起腹忽,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤谢鹊,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后留凭,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體佃扼,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年蔼夜,在試婚紗的時候發(fā)現(xiàn)自己被綠了兼耀。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡求冷,死狀恐怖瘤运,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情匠题,我是刑警寧澤拯坟,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站韭山,受9級特大地震影響郁季,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜钱磅,卻給世界環(huán)境...
    茶點故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一梦裂、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盖淡,春花似錦年柠、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽答憔。三九已至,卻和暖如春掀抹,著一層夾襖步出監(jiān)牢的瞬間攀唯,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工渴丸, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留侯嘀,地道東北人。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓谱轨,卻偏偏與公主長得像戒幔,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子土童,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,033評論 2 355

推薦閱讀更多精彩內(nèi)容