Twitter雪花演算法SnowFlake演算法的java實現

揍敵玩玩發表於2019-02-26

1 二進位制初識

1.1 二進位制概念

二進位制是計算技術中廣泛採用的一種數制。二進位制資料是用0和1兩個數碼來表示的數。
它的基數為2,進位規則是“逢二進一”,借位規則是“借一當二”,由18世紀德國數理哲學大師萊布尼茲發現。
當前的計算機系統使用的基本上是二進位制系統,資料在計算機中主要是以補碼的形式儲存的。
計算機中的二進位制則是一個非常微小的開關,用“開”來表示1,“關”來表示0。
複製程式碼

1.2 運演算法則

二進位制的運算算術運算
二進位制的加法:0+0=0,0+1=1 ,1+0=1, 1+1=10(向高位進位);例:7=111;10=1010;3=11
二進位制的減法:0-0=0,0-1=1(向高位借位) 1-0=1,1-1=0 (模二加運算或異或運算) ;
二進位制的乘法:0 * 0 = 0; 0 * 1 = 0; 1 * 0 = 0; 1 * 1 = 1;
二進位制的除法:0÷0 = 0,0÷1 = 0,1÷0 = 0 (無意義),1÷1 = 1 ;
邏輯運算二進位制的或運算:遇1得1 二進位制的與運算:遇0得0 二進位制的非運算:各位取反。
複製程式碼

1.3 位

資料儲存的最小單位。每個二進位制數字0或者1就是1個位;
複製程式碼

1.4 位元組

8個位構成一個位元組;即:1 byte (位元組)= 8 bit(位);

     1 KB = 1024 B(位元組);

     1 MB = 1024 KB;   (2^10 B)

     1 GB = 1024 MB;   (2^20 B)

     1 TB = 1024 GB;   (2^30 B)
複製程式碼

1.5 字元

a、A、中、+、*、の......均表示一個字元;

    一般 utf-8 編碼下,一個漢字 字元 佔用 3 個 位元組;

    一般 gbk 編碼下,一個漢字  字元  佔用 2 個 位元組;
複製程式碼

1.6 字符集

即各種各個字元的集合,也就是說哪些漢字,字母(A、b、c)和符號(空格、引號..)會被收入標準中;
複製程式碼

1.7 java語言的8大基本資料型別

| 資料型別 | 位元組長度(單位:byte) | 位長度(單位:bit)|
|------- |--------------------|-----------------|
| int    | 4                  | 32              |
| byte   | 1                  | 8               |
| short  | 2                  | 16              |
| long   | 8                  | 64              |
| float  | 4                  | 32              |
| double | 8                  | 64              |
| boolean| 1                  | 8               |
| char   | 2                  | 16              |
複製程式碼

1.8 二進位制原碼、反碼、補碼

正數 負數
原碼 原碼 原碼
反碼 原碼 原碼符號位外按位取反
補碼 原碼 反碼+1

1.9 有符號數和無符號數

無符號數中,所有的位都用於直接表示該值的大小。
有符號數中最高位用於表示正負。
例:
8位2進製表示的:
無符號數的範圍為0(00000000B) ~ 255 (11111111B);
有符號數的範圍為-128(10000000B) ~ 127 (01111111B);
255 = 1*2^7 + 1*2^6 + 1*2^5 +1*2^4 +1*2^3 +1*2^2 +1*2^1 +1*2^0;
127 = 1*2^6 + 1*2^5 +1*2^4 +1*2^3 +1*2^2 +1*2^1 +1*2^0;
複製程式碼

疑問:為什麼不是-127 ~ 127 ?

算機中資料用補碼錶示,利用補碼統一了符號位與數值位的運算,同時解決了+0、-0問題,將空出來的二進位制原碼1000 0000表示為-128,這也符合自身邏輯意義的完整性。因此八位二進位制數表示範圍為-128~+127。

對於n位二進位制數原、反、補碼範圍:

Twitter雪花演算法SnowFlake演算法的java實現
你會發現,補碼比其它碼多一位,這是為什麼呢?問題出在0上。

[+0]原碼=0000 0000,   [-0]原碼=1000 0000

[+0]反碼=0000 0000,   [-0]反碼=1111 1111

[+0]補碼=0000 0000,   [-0]補碼=0000 0000

反碼錶示法規定:正數的反碼與其原碼相同。負數的反碼是對其原碼逐位取反,但符號位除外。

在規定中,8位二進位制碼能表示的反碼範圍是-127~127。

-128沒有反碼。

那麼,為什麼規定-128沒有反碼呢?下面解釋。

首先看-0,[-0]原碼=1000 000,其中1是符號位,根據反碼規定,算出[-0]反碼=1111 1111,

再看-128,[-128]原碼=1000 000,假如讓-128也有反碼,根據反碼規定,則[-128]反碼=1111 1111,

你會發現,-128的反碼和-0的反碼相同,所以為了避免面混淆,有了-0,便不能有-128,這是反碼規則決定的。
複製程式碼

因此八位二進位制表示的範圍為:-128~0~127。此處的0為正0,負0表示了-128。

注意:

-128的8位補碼是:1000 0000B,換算成十進位制就是 128。
負數的補碼,是用“模”計算出來的,
即:
[X]補 = 256 - |X| = 256- |-128| = 128
複製程式碼

2 理解分散式id生成演算法SnowFlake

2.1 概述

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

Twitter雪花演算法SnowFlake演算法的java實現

1) 1位,不用。二進位制中最高位為1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0
2) 41位,用來記錄時間戳(毫秒)。
3) 41位可以表示2^41−1個數字,如果只用來表示正整數(計算機中正數包含0),可以表示的數值範圍是:0 至 2^41−1,減1是因為可表示的數值範圍是從0開始算的,而不是1。
也就是說41位可以表示2^41−1個毫秒的值,轉化成單位年則是(2^41−1)/(1000∗60∗60∗24∗365)=69年
4) 10位,用來記錄工作機器id。
可以部署在2^10=1024個節點,包括5位datacenterId和5位workerId
5) 5位(bit)可以表示的最大正整數是2^5−1=31,即可以用0、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId
6) 12位,序列號,用來記錄同毫秒內產生的不同id。
12位(bit)可以表示的最大正整數是2^12−1=4095,即可以用0、1、2、3、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號
由於在Java中64bit的整數是long型別,所以在Java中SnowFlake演算法生成的id就是long來儲存的。
複製程式碼

2.2 雪花演算法的作用

SnowFlake可以保證:

所有生成的id按時間趨勢遞增

整個分散式系統內不會產生重複id(因為有datacenterId和workerId來做區分)
複製程式碼

2.3 雪花演算法java程式碼展示

下面解讀一下java版的雪花演算法:

	public class SnowFlake {

    /**
     * 起始的時間戳:這個時間戳自己隨意獲取,比如自己程式碼的時間戳
     */
    private final static long START_STMP = 1543903501000L;

    /**
     * 每一部分佔用的位數
     */
    private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
    private final static long MACHINE_BIT = 5;  //機器標識佔用的位數
    private final static long DATACENTER_BIT = 5;//資料中心佔用的位數

    /**
     * 每一部分的最大值:先進行左移運算,再同-1進行異或運算;異或:相同位置相同結果為0,不同結果為1
     */
     /** 用位運算計算出最大支援的資料中心數量:31 */
    private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT);
    
    /** 用位運算計算出最大支援的機器數量:31 */
    private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
    
    /** 用位運算計算出12位能儲存的最大正整數:4095 */
    private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);

    /**
     * 每一部分向左的位移
     */
     
     /** 機器標誌較序列號的偏移量 */
    private final static long MACHINE_LEFT = SEQUENCE_BIT;
    
    /** 資料中心較機器標誌的偏移量 */
    private final static long DATACENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
    
    /** 時間戳較資料中心的偏移量 */
    private final static long TIMESTMP_LEFT = DATACENTER_LEFT + DATACENTER_BIT;

    private static long datacenterId;  //資料中心
    private static long machineId;    //機器標識
    private static long sequence = 0L; //序列號
    private static long lastStmp = -1L;//上一次時間戳

	 /** 此處無參構造私有,同時沒有給出有參構造,在於避免以下兩點問題:
	 	  1、私有化避免了通過new的方式進行呼叫,主要是解決了在for迴圈中通過new的方式呼叫產生的id不一定唯一問題問題,因為用於			 記錄上一次時間戳的lastStmp永遠無法得到比對;
	 	  2、沒有給出有參構造在第一點的基礎上考慮了一套分散式系統產生的唯一序列號應該是基於相同的引數
	  */
    private SnowFlake(){}

    /**
     * 產生下一個ID
     *
     * @return
     */
    public static synchronized long nextId() {
    	  /** 獲取當前時間戳 */
        long currStmp = getNewstmp();
        
        /** 如果當前時間戳小於上次時間戳則丟擲異常 */
        if (currStmp < lastStmp) {
            throw new RuntimeException("Clock moved backwards.  Refusing to generate id");
        }
		 /** 相同毫秒內 */
        if (currStmp == lastStmp) {
            //相同毫秒內,序列號自增
            sequence = (sequence + 1) & MAX_SEQUENCE;
            //同一毫秒的序列數已經達到最大
            if (sequence == 0L) {
            
            		/** 獲取下一時間的時間戳並賦值給當前時間戳 */
                currStmp = getNextMill();
            }
        } else {
            //不同毫秒內,序列號置為0
            sequence = 0L;
        }
		 /** 當前時間戳存檔記錄,用於下次產生id時對比是否為相同時間戳 */
        lastStmp = currStmp;


        return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | datacenterId << DATACENTER_LEFT      //資料中心部分
                | machineId << MACHINE_LEFT            //機器標識部分
                | sequence;                            //序列號部分
    }

    private static long getNextMill() {
        long mill = getNewstmp();
        while (mill <= lastStmp) {
            mill = getNewstmp();
        }
        return mill;
    }

    private static long getNewstmp() {
        return System.currentTimeMillis();
    }

}
複製程式碼

2.4 雪花演算法demo展示

下面就上述程式碼給出案例進行相關位運算 所有引數展示:

當前生成的id :1143315521077248
currStmp:1544176088662
START_STMP:1543903501000
SEQUENCE_BIT:12
MACHINE_BIT:5
DATACENTER_BIT:5
MAX_DATACENTER_NUM:31
MAX_MACHINE_NUM:31
MAX_SEQUENCE:4095
MACHINE_LEFT:12
DATACENTER_LEFT:17
TIMESTMP_LEFT:22
datacenterId:0
machineId:0
sequence:0
lastStmp:1544176088662
currStmp - START_STMP:272587662
(currStmp - START_STMP) << TIMESTMP_LEFT:1143315521077248
datacenterId << DATACENTER_LEFT:0
machineId << MACHINE_LEFT:0
sequence:0
(currStmp - START_STMP) << TIMESTMP_LEFT | datacenterId << DATACENTER_LEFT:1143315521077248
(currStmp - START_STMP) << TIMESTMP_LEFT | datacenterId << DATACENTER_LEFT| machineId << MACHINE_LEFT:1143315521077248
(currStmp - START_STMP) << TIMESTMP_LEFT | datacenterId << DATACENTER_LEFT| machineId << MACHINE_LEFT | sequence:1143315521077248
複製程式碼

2.5 雪花演算法中的實際運算

1) MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT):資料中心最大數量;
先進行-1的左移5位,得到的結果再同-1進行異或運算:
由於我們知道1的補碼+(-1)的補碼=0;
8位2進位制1的表示表示為:00000001;
由此得出xxxxxxxx(代表-1的補碼):

   00000001
 + xxxxxxxx
-----------
 = 00000000
 
 從而得出:xxxxxxxx = 11111111;(溢位的最高位捨棄)
 -1L << DATACENTER_BIT:-1左移5位:高位捨棄,低位用0補齊,
 則:為11100000;
 -1L ^ 11100000:-1異或11100000
 則:
   11111111
 ^ 11100000
 ----------
 = 00011111
 最高位為0,代表正數,即:2^4 +2^3 +2^2 +2^1 +2^0 = 31;
 
 2) MAX_MACHINE_NUM 同理;
 3)MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT):最大序列號,計算12位能耐儲存的最大正整數:
 -1L << SEQUENCE_BIT:-1向左偏移12位:1111000000000000;
 -1 ^ 1111000000000000:
   1111111111111111
 ^ 1111000000000000
 ------------------
 = 0000111111111111
 最高位為0,代表正數,同上計算出為:4095;
 4) (currStmp - START_STMP) << TIMESTMP_LEFT:272587662向左偏移22位:
 即:0001 0000 0011 1111 0101 1011 1000 1110左移:22位,左移可能造成高位資料丟失,故先把0001 0000 0011 1111 0101 1011 1000 1110用64位進行表示,則為:
  0000 0000 0000 0000 0000 0000 0000 0000 0001 0000 0011 1111 0101 1011 1000 1110;再進行左移22位為:
 0000 0000 0000 0100 0000 1111 1101 0110 1110 0011 1000 0000 0000 0000 0000 0000;
 經如下計算換算成10進位制:1*2^41 + 0*2^40 +0*2^39+ …… +0*2^2 +0*2^1 +0*2^0 = 1143315521077248;
 其他較長的位運算同4)進行即可得出正確驗證。
 5)(currStmp - START_STMP) << TIMESTMP_LEFT | datacenterId << DATACENTER_LEFT| machineId << MACHINE_LEFT:1143315521077248同0偏移12位的結果做或運算,因為0的偏移結果還是0,再進行或運算的結果即為1143315521077248本身;
 6)(currStmp - START_STMP) << TIMESTMP_LEFT | datacenterId << DATACENTER_LEFT| machineId << MACHINE_LEFT | sequence同5)即得證結果為1143315521077248;
 注:至於最後返回(return)的的表示式中的或運算(|),我們只要知道二進位制中1的位置,相同位置有一個為1即為1,其餘部分補0即可。
複製程式碼

3 雪花演算法中值得考慮的問題

雪花演算法中的溢位問題

先來看一段程式碼:sequence = (sequence + 1) & MAX_SEQUENCE;
替換一下引數為:sequence = (sequence + 1) &  (-1L ^ (-1L << SEQUENCE_BIT))
帶入常量為:sequence = (sequence + 1) &  (-1L ^ (-1L << 12))
化簡得:sequence = (sequence + 1) & 4095;
用控制檯列印結果展示這段程式碼解決的問題:
        //計算12位能耐儲存的最大正整數,相當於:2^12-1 = 4095
        long seqMask = -1L ^ (-1L << 12L);
        System.out.println("seqMask: "+seqMask);
        System.out.println(1L & seqMask);
        System.out.println(2L & seqMask);
        System.out.println(3L & seqMask);
        System.out.println(4L & seqMask);
        System.out.println(4095L & seqMask);
        System.out.println(4096L & seqMask);
        System.out.println(4097L & seqMask);
        System.out.println(4098L & seqMask);

        
        /**
        seqMask: 4095
        1
        2
        3
        4
        4095
        0
        1
        2
        */
複製程式碼

帶入雪花演算法中去看,問題就是:當同一毫秒產生的序列號已經最大了,該怎麼辦?

上述內容已經給出了明確的解決方案,接下來我們去看另外一個很實際的問題;

雪花演算法中的夏令時問題

我們所說的夏令時實際上包括兩類:夏令時和冬令時

夏令時(1:00 -> 3:00 AM)
往後撥一個小時,直接從1點變到3點,也就是說我們要比原來提前一個小時和美國人開會。
冬令時(1:00 -> 1:00 -> 2:00 AM)
往前撥一個小時,要過兩個1點,這時比平常晚一個小時。
複製程式碼

由此可見夏令時從1點跳到3點在雪花演算法中沒有什麼影響,但是在冬令時要經歷兩個相同的時間段並使用相同的時間戳和演算法引數進行運算就要出問題了。 忽略掉具體的實現邏輯,單單看返回結果的構造: 

return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分
                | datacenterId << DATACENTER_LEFT      //資料中心部分
                | machineId << MACHINE_LEFT            //機器標識部分
                | sequence;                            //序列號部分
複製程式碼

主要問題在於currStmp - START_STMP會重複,藉此入手是否有可行的解決方案?

猜想1、在不影響後續時間戳差值及以前資料的情況下能否再產生一個新的時間戳差值?比如上述引數可食用的年限為69年,這裡能不能使用69年以後的資料?
猜想2、在冬令時改變序列號的計算演算法,使用4095以後未使用的資料;
複製程式碼

至此本次關於雪花演算法的分享到此就結束了,歡迎大家在下方積極評論與交流,下期我們再會。

相關文章