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

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 ?java

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

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

你會發現,補碼比其它碼多一位,這是爲何呢?問題出在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。分佈式

注意:ui

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

2 理解分佈式id生成算法SnowFlake

2.1 概述

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

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、一、二、三、....31這32個數字,來表示不一樣的datecenterId或workerId
6) 12位,序列號,用來記錄同毫秒內產生的不一樣id。
12位(bit)能夠表示的最大正整數是2^12−1=4095,便可以用0、一、二、三、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號
因爲在Java中64bit的整數是long類型,因此在Java中SnowFlake算法生成的id就是long來存儲的。
複製代碼

2.2 雪花算法的做用

SnowFlake能夠保證:spa

全部生成的id按時間趨勢遞增

整個分佈式系統內不會產生重複id(由於有datacenterId和workerId來作區分)
複製代碼

2.3 雪花算法java代碼展現

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

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;//上一次時間戳

	 /** 此處無參構造私有,同時沒有給出有參構造,在於避免如下兩點問題:
	 	  一、私有化避免了經過new的方式進行調用,主要是解決了在for循環中經過new的方式調用產生的id不必定惟一問題問題,由於用於			 記錄上一次時間戳的lastStmp永遠沒法獲得比對;
	 	  二、沒有給出有參構造在第一點的基礎上考慮了一套分佈式系統產生的惟一序列號應該是基於相同的參數
	  */
    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展現

下面就上述代碼給出案例進行相關位運算 全部參數展現:cdn

當前生成的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
        */
複製代碼

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

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

雪花算法中的夏令時問題

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

夏令時(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會重複,藉此入手是否有可行的解決方案?

猜測一、在不影響後續時間戳差值及之前數據的狀況下可否再產生一個新的時間戳差值?好比上述參數可食用的年限爲69年,這裏能不能使用69年之後的數據?
猜測二、在冬令時改變序列號的計算算法,使用4095之後未使用的數據;
複製代碼

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

相關文章
相關標籤/搜索