snowflake原理解析


Snowflake

世界上沒有兩片完全相同的雪花。

​ — twitter

Snowflake原理

這種方案把64-bit分別划分成多段,分開來標示機器、時間等,比如在snowflake中的64-bit分別表示如下圖所示:

在java里,64bit正好是long類型的大小。

41-bit的時間可以表示(1L<<41)/(1000ms * 60s * 60m * 24h * 365d)=69年的時間,10-bit機器可以分別表示1024台機器。如果我們對IDC划分有需求,還可以將10-bit分5-bit給IDC,分5-bit給工作機器。這樣就可以表示32個IDC,每個IDC下可以有32台機器,可以根據自身需求定義。12個自增序列號可以表示212個ID,理論上snowflake方案的QPS約為212=4096/ms, 也就是409.6w/s,這種分配方式可以保證在任何一個IDC的任何一台機器在任意毫秒內生成的ID都是不同的。

+-----------+--------------------------+---------------------+----------------------+----------------+

| sign | time_stamp | datacenter | worker node | sequence |

+-----------+--------------------------+---------------------+----------------------+----------------+

​ 1bit 41 bits 5bits 5bits 12bits

+-----------+--------------------------+---------------------+----------------------+----------------+

  • 1位,不用。二進制中最高位為1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0

  • 41位,用來記錄時間戳(毫秒)。

    • 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$年
  • 10位,用來記錄工作機器id。

    • 可以部署在$2^{10} = 1024$個節點,包括5位datacenterId5位workerId
    • 5位(bit)可以表示的最大正整數是$2^{5}-1 = 31$,即可以用0、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId
  • 12位,序列號,用來記錄同毫秒內產生的不同id。

    • 12位(bit)可以表示的最大正整數是$2^{12}-1 = 4095$,即可以用0、1、2、3、....4094這4095個數字,來表示同一機器同一時間截(毫秒)內產生的4095個ID序號

位操作解釋

    /**
     * define the initial bit for each part of 64 bits of one Id
     */
    private final long sequenceBits = 12L;
    private final long datacenterIdBits = 5L;
    private final long workerIdBits = 5L;
    private final long timestampBits = 41L;

    /*
     * max values of workerId, datacenterId and sequence
     * 11111111 11111111 11111111 11111111  // -1 in binary format
     */
    // 2^5-1 = 31
    private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
    // 2^10-1 = 1023
    private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
    // 2^12-1 = 4095
    private final long maxSequence = -1L ^ (-1L << sequenceBits);

先看snowflake里面定義成員變量的一個神操作,為什么這么定義呢?需要先了解下二進制位操作的原理。

負數的二進制表示

在計算機中,負數的二進制是用補碼來表示的。
假設我是用Java中的int類型來存儲數字的,
int類型的大小是32個二進制位(bit),即4個字節(byte)。(1 byte = 8 bit)
那么十進制數字3在二進制中的表示應該是這樣的:

00000000 00000000 00000000 00000011
// 3的二進制表示,就是原碼

那數字-3在二進制中應該如何表示?
我們可以反過來想想,因為-3+3=0,
在二進制運算中把-3的二進制看成未知數x來求解
求解算式的二進制表示如下:


   00000000 00000000 00000000 00000011 //3,原碼
+  xxxxxxxx xxxxxxxx xxxxxxxx xxxxxxxx //-3,補碼
-----------------------------------------------
   00000000 00000000 00000000 00000000

反推x的值,3的二進制加上什么值才使結果變成00000000 00000000 00000000 00000000?:

  00000000 00000000 00000000 00000011 //3,原碼                         
+ 11111111 11111111 11111111 11111101 //-3,補碼
-----------------------------------------------
 1 00000000 00000000 00000000 00000000

反推的思路是3的二進制數從最低位開始逐位加1,使溢出的1不斷向高位溢出,直到溢出到第33位。然后由於int類型最多只能保存32個二進制位,所以最高位的1溢出了,剩下的32位就成了(十進制的)0

補碼的意義就是可以拿補碼和原碼(3的二進制)相加,最終加出一個“溢出的0”

以上是理解的過程,實際中記住公式就很容易算出來:

  • 補碼 = 反碼 + 1
  • 補碼 = (原碼 - 1)再取反碼

因此-1的二進制應該這樣算:

00000000 00000000 00000000 00000001 //原碼:1的二進制
11111111 11111111 11111111 11111110 //取反碼:1的二進制的反碼
11111111 11111111 11111111 11111111 //加1:-1的二進制表示(補碼)

用位運算 得出n個bit能存儲最大值

private long workerIdBits = 5L;
// 2^5-1 = 31
private long maxWorkerId = -1L ^ (-1L << workerIdBits);      

其中:

^ 操作符是 異或 操作, 即:如果a、b兩個值不相同,則異或結果為1。如果a、b兩個值相同,異或結果為0。

異或[1]也叫半加運算,其運算法則相當於不帶進位的二進制加法:二進制下用1表示真,0表示假,則異或的運算法則為:0⊕0=0,1⊕0=1,0⊕1=1,1⊕1=0(同為0,異為1),這些法則與加法是相同的,只是不帶進位,所以異或常被認作不進位加法。

延伸:巧用異或算法

1-1000放在含有1001個元素的數組中,只有唯一的一個元素值重復,其它均只出現
一次。每個數組元素只能訪問一次,設計一個算法,將它找出來;不用輔助存儲空
間,能否設計一個算法實現?
解法一、顯然已經有人提出了一個比較精彩的解法,將所有數加起來,減去1+2+…+1000的和。
這個算法已經足夠完美了,相信出題者的標准答案也就是這個算法,唯一的問題是,如果數列過大,則可能會導致溢出。

解法二、異或就沒有這個問題,並且性能更好。
將所有的數全部異或,得到的結果與1231000的結果進行異或,得到的結果就是重復數。

google面試題的變形:一個數組存放若干整數,一個數出現奇數次,其余數均出現偶數次,找出這個出現奇數次的數?

Leecode:https://leetcode-cn.com/problems/single-number/solution/

@Test
public void fun() {
	int a[] = { 22, 38,38, 22,22, 4, 4, 11, 11 };
	int temp = 0;
	for (int i = 0; i < a.length; i++) {
		temp ^= a[i];
	}
	System.out.println(temp);
}

解法有很多,但是最好的和上面一樣,就是把所有數異或,最后結果就是要找的,原理同上!!

<<左移 操作

private long workerIdBits = 5L;
private long maxWorkerId = -1L ^ (-1L << workerIdBits);       

所以long maxWorkerId = -1L ^ (-1L << 5L)的二進制運算過程如下:

  • -1 左移 5,得結果a
      11111111 11111111 11111111 11111111 //-1的二進制表示(補碼)
11111 11111111 11111111 11111111 11100000 //高位溢出的不要,低位補0
      11111111 11111111 11111111 11100000 //結果a
  • -1 異或 a
  11111111 11111111 11111111 11111111 //-1的二進制表示(補碼)
^ 11111111 11111111 11111111 11100000 //兩個操作數的位中,相同則為0,不同則為1
  ---------------------------------------------------------------------------
  00000000 00000000 00000000 00011111 //最終結果31

最終結果是31,二進制00000000 00000000 00000000 00011111轉十進制可以這么算:

2^4 + 2^3 + 2^2 + 2^1 + 2^0 = 16 + 8 + 4 +2 +1 = 31

所以這一通操作其實是通過位運算計算出來5bit的work id 最多能存儲31個值,也就是最多支持31個節點。

位與 操作防止溢出

我們在跨毫秒時,序列號總是歸0,會使得序列號為0的ID比較多,導致生成的ID取模后不均勻。優化的點是,序列號不是每次都歸0,而是歸一個0到100的隨機數。

//如果當前操作落在同一個ms(timestamp位相同)的話
if (lastTimestamp == currentTimestamp) {
  					//sequence++ 且 保證不溢出,下面測試可知道如果溢出了maxSequence就會變成0
            sequence = (sequence + 1) & maxSequence;
            if (sequence == 0) {
                // overflow: greater than max sequence
                sequence = RANDOM.nextInt(100);
                currentTimestamp = waitNextMillis(lastTimestamp);
            }
        } else {
            //如果是新的ms開始,防止並發不夠每次都是0
            //sequence = 0L;
            sequence = RANDOM.nextInt(100);

        }

關於這里的溢出操作分別用不同的值測試一下,就知道原理了:

long seqMask = -1L ^ (-1L << 12L); //計算12位能耐存儲的最大正整數,相當於:2^12-1 = 4095
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
*/

因為maxSequence 我們選用12bit最大值是4095,這段代碼通過位與運算保證計算的結果范圍始終是 0-4095 !

生成id的核心代碼

long id = ((currentTimestamp - epoch) << timestampShift) |
                (datacenterId << datacenterIdShift) |
                (workerId << workerIdShift) |
                sequence;

計算分為2部分:

  1. << 每個對應部分的id 左移對應的bits;

  2. 管道符號|在Java中也是一個位運算符。其含義是:
    x的第n位和y的第n位 只要有一個是1,則結果的第n位也為1,否則為0,因此,我們對四個數的位或運算如下:

1  |                    41                        |  5  |   5  |     12      
    
  0|0001100 10100010 10111110 10001001 01011100 00|00000|0 0000|0000 00000000
  0|000000‭0 00000000 00000000 00000000 00000000 00|10001|0 0000|0000 00000000
  0|0000000 00000000 00000000 00000000 00000000 00|00000|1 1001|0000 00000000
or0|0000000 00000000 00000000 00000000 00000000 00|00000|0 0000|‭0000 00000000‬
------------------------------------------------------------------------------------------
  0|0001100 10100010 10111110 10001001 01011100 00|10001|1 1001|‭0000 00000000‬ 
  //結果:910499571847892992

位或運算角度簡化看是這樣的視角

1  |                    41                        |  5  |   5  |     12      
    
  0|0001100 10100010 10111110 10001001 01011100 00|     |      |      //la
  0|                                              |10001|      |      //lb
  0|                                              |     |1 1001|      //lc
or0|                                              |     |      |‭0000 00000000‬ //seq
------------------------------------------------------------------------------------------
   0|0001100 10100010 10111110 10001001 01011100 00|10001|1 1001|‭0000 00000000‬ 
   //結果:910499571847892992

上面的64位我按1、41、5、5、12的位數截開了,方便觀察。

  • 縱向觀察發現:
    • 在41位那一段,除了la一行有值,其它行(lb、lc、seq)都是0
    • 在左起第一個5位那一段,除了lb一行有值,其它行都是0
    • 在左起第二個5位那一段,除了lc一行有值,其它行都是0
    • 按照這規律,如果seq是0以外的其它值,12位那段也會有值的,其它行都是0
  • 橫向觀察發現:
    • 在la行,由於左移了5+5+12位,5、5、12這三段都補0了,所以la行除了41那段外,其它肯定都是0
    • 同理,lb、lc、seq行也以此類推
    • 正因為左移的操作,使四個不同的值移到了SnowFlake理論上相應的位置,然后四行做位或運算(只要有1結果就是1),就把4段的二進制數合並成一個二進制數。

結論:
所以,在這段代碼中左移運算是為了將數值移動到對應的段(41、5、5,12那段因為本來就在最右,因此不用左移)。然后對每個左移后的值(la、lb、lc、seq)做位或運算,是為了把各個短的數據合並起來,合並成一個二進制數。最后轉換成10進制,就是最終生成的id。

延伸:long和double底層

問題: java中 long 和double都是64位。為什么double表示的范圍大那么多呢?

標准答案是這樣子的:

double是n*2^m(n乘以2的m次方)這種形式存儲的,只需要記錄n和m兩個數就行了,m的值影響范圍大,所以表示的范圍比long大。
但是m越大,n的精度就越小,所以double並不能把它所表示的范圍里的所有數都能精確表示出來,而long就可以。

貼上一些整數類型的范圍:
1.整型 (一個字節占8位)
類型 存儲需求 bit數 取值范圍 備注
int 4字節 48 (32) -231~231-1
short 2字節 2
8 (16) -215~215-1
long 8字節 88 (64) -263~263-1
byte 1字節 1
8 (8) -27~27-1 = -128~127

2.浮點型
類型 存儲需求 bit數 取值范圍 備注
float 4字節 48 (32) 3.4028235E38 ~= 3.410^38
double 8字節 88 (64) 1.7976931348623157E308 ~=1.710^308

從范圍來看double和long完全不是一個級別的了吧?long最大為=263-1,而double為21024。

Snowflake的優缺點

優點:

  • 毫秒數在高位,自增序列在低位,整個ID都是趨勢遞增的。
  • 不依賴數據庫等第三方系統,以服務的方式部署,穩定性更高,生成ID的性能也是非常高的。
  • 可以根據自身業務特性分配bit位,非常靈活。

缺點:

  • 強依賴機器時鍾,如果機器上時鍾回撥,會導致發號重復或者服務會處於不可用狀態。
  • 我們通過5ms的等待來兼容ntp的同步,如果大於5ms就報錯。

服務部署

部署模式支持2種

  1. 部署zookeeper, 配置zk地址,然后啟動Spring boot

  2. 也可以把snowflake單獨作為包引入項目,獨立使用

配置方式

  1. Rest Server的配置在server/src/main/resources/application.properties中:
配置項 含義 默認值
spring.application.name web服務名 default
server.port web服務注冊端口
snowflake.zk.address zk地址
  1. 如果只是配置snowflake這個單獨的模塊,可以參考snowflake/src/test/resources/snowflake.properties中:
    | 配置項 | 含義 | 默認值 |
    | ------------------------- | ----------------------------- | ------ |
    | snowflake.name | snowflake服務名 | default|
    | snowflake.node.port | snowflake服務注冊端口 | |
    | snowflake.zk.address | zk地址 | |

遠程調用方式

  1. HTTP調用拿一個id:

curl http://localhost:8789/api/snowflake/get

response

{
    "code":0,
    "message":"ok",
    "content":{
        "id":9546332062617603,
        "status":"SUCCESS"
    }
}
  1. HTTP調用解析一個id:

curl http://localhost:8789/api/snowflake/decode?snowflakeId=9546332062617603

response

{
    "code":0,
    "message":"ok",
    "content":{
        "format":"2019-10-27 08:13:42.926, #3, @(0,1)",
        "timestamp":"2019-10-27 08:13:42.926",
        "datacenterId":0,
        "workerId":1,
        "sequenceId":3
    }
}

歡迎關注我的公眾號:好奇心森林
Wechat


  1. https://www.cnblogs.com/fuck1/p/5899402.html ↩︎


免責聲明!

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



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