冷飯新炒:理解Snowflake演算法的實現原理
阿新 • • 發佈:2020-08-10
## 前提
`Snowflake`(雪花)是`Twitter`開源的高效能`ID`生成演算法(服務)。
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-1.png)
上圖是`Snowflake`的`Github`倉庫,`master`分支中的`REAEMDE`檔案中提示:初始版本於`2010`年釋出,基於`Apache Thrift`,早於`Finagle`(這裡的`Finagle`是`Twitter`上用於`RPC`服務的構建模組)釋出,而`Twitter`內部使用的`Snowflake`是一個完全重寫的程式,在很大程度上依靠`Twitter`上的現有基礎架構來執行。
而`2010`年釋出的初版`Snowflake`原始碼是使用`Scala`語言編寫的,歸檔於`scala_28`分支。換言之,**大家目前使用的`Snowflake`演算法原版或者改良版已經是十年前(當前是`2020`年)的產物,不得不說這個演算法確實比較厲害**。`scala_28`分支中有介紹該演算法的動機和要求,這裡簡單摘錄一下:
**動機:**
- `Cassandra`中沒有生成順序`ID`的工具,`Twitter`由使用`MySQL`轉向使用`Cassandra`的時候需要一種新的方式來生成`ID`(印證了架構不是設計出來,而是基於業務場景迭代出來)。
**要求:**
- 高效能:每秒每個程序至少產生`10K`個`ID`,加上網路延遲響應速度要在`2ms`內。
- 順序性:具備按照時間的自增趨勢,可以直接排序。
- 緊湊性:保持生成的`ID`的長度在`64 bit`或更短。
- 高可用:`ID`生成方案需要和儲存服務一樣高可用。
下面就`Snowflake`的原始碼分析一下他的實現原理。
## Snowflake方案簡述
`Snowflake`在初版設計方案是:
- 時間:`41 bit`長度,使用毫秒級別精度,帶有一個自定義`epoch`,那麼可以使用大概`69`年。
- 可配置的機器`ID`:`10 bit`長度,可以滿足`1024`個機器使用。
- 序列號:`12 bit`長度,可以在`4096`個數字中隨機取值,從而避免單個機器在`1 ms`內生成重複的序列號。
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-2.png)
但是在實際原始碼實現中,`Snowflake`把`10 bit`的可配置的機器`ID`拆分為`5 bit`的`Worker ID`(這個可以理解為原來的機器`ID`)和`5 bit`的`Data Center ID`(資料中心`ID`),詳情見`IdWorker.scala`:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-3.png)
也就是說,支援配置最多`32`個機器`ID`和最多`32`個數據中心`ID`:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-4.png)
由於演算法是`Scala`語言編寫,是依賴於`JVM`的語言,返回的`ID`值為`Long`型別,也就是`64 bit`的整數,原來的演算法生成序列中只使用了`63 bit`的長度,要返回的是無符號數,所以在高位補一個`0`(佔用`1 bit`),那麼加起來整個`ID`的長度就是`64 bit`:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-5.png)
其中:
- `41 bit`毫秒級別時間戳的取值範圍是:`[0, 2^41 - 1]` => `0 ~ 2199023255551`,一共`2199023255552`個數字。
- `5 bit`機器`ID`的取值範圍是:`[0, 2^5 - 1]` => `0 ~ 31`,一共`32`個數字。
- `5 bit`資料中心`ID`的取值範圍是:`[0, 2^5 - 1]` => `0 ~ 31`,一共`32`個數字。
- `12 bit`序列號的取值範圍是:`[0, 2^12 - 1]` => `0 ~ 4095`,一共`4096`個數字。
那麼理論上可以生成`2199023255552 * 32 * 32 * 4096`個完全不同的`ID`值。
`Snowflake`演算法還有一個明顯的特徵:**依賴於系統時鐘**。`41 bit`長度毫秒級別的時間來源於系統時間戳,所以必須保證系統時間是向前遞進,不能發生**時鐘回撥**(通說來說就是不能在同一個時刻產生多個相同的時間戳或者產生了過去的時間戳)。一旦發生時鐘回撥,`Snowflake`會拒絕生成下一個`ID`。
## 位運算知識補充
`Snowflake`演算法中使用了大量的位運算。由於整數的補碼才是在計算機中的儲存形式,`Java`或者`Scala`中的整型都使用補碼錶示,這裡稍微提一下原碼和補碼的知識。
- 原碼用於閱讀,補碼用於計算。
- 正數的補碼與其原碼相同。
- 負數的補碼是除最高位其他所有位取反,然後加`1`(反碼加`1`),而負數的補碼還原為原碼也是使用這個方式。
- `+0`的原碼是`0000 0000`,而`-0`的原碼是`1000 0000`,補碼只有一個`0`值,用`0000 0000`表示,這一點很重要,補碼的`0`沒有二義性。
簡單來看就是這樣:
```shell
* [+ 11] 原碼 = [0000 1011] 補碼 = [0000 1011]
* [- 11] 原碼 = [1000 1011] 補碼 = [1111 0101]
* [- 11]的補碼計算過程:
原碼 1000 1011
除了最高位其他位取反 1111 0100
加1 1111 0101 (補碼)
```
使用原碼、反碼在計算的時候得到的不一定是準確的值,而使用補碼的時候計算結果才是正確的,記住這個結論即可,這裡不在舉例。由於`Snowflake`的`ID`生成方案中,除了最高位,其他四個部分都是無符號整數,所以四個部分的整數**使用補碼進行位運算的效率會比較高,也只有這樣才能滿足Snowflake高效能設計的初衷**。`Snowflake`演算法中使用了幾種位運算:異或(`^`)、按位與(`&`)、按位或(`|`)和帶符號左移(`<<`)。
### 異或
異或的運算規則是:`0^0=0` `0^1=1` `1^0=1` `1^1=0`,也就是位不同則結果為1,位相同則結果為0。主要作用是:
- 特定位翻轉,也就是一個數和`N`個位都為`1`的數進行異或操作,這對應的`N`個位都會翻轉,例如`0100 & 1111`,結果就是`1011`。
- 與`0`項異或,則結果和原來的值一致。
- 兩數的值互動:`a=a^b` `b=b^a` `a=a^b`,這三個操作完成之後,`a`和`b`的值完成交換。
這裡推演一下最後一條:
```shell
* [+ 11] 原碼 = [0000 1011] 補碼 = [0000 1011] a
* [- 11] 原碼 = [1000 1011] 補碼 = [1111 0101] b
a=a^b 0000 1011
1111 0101
---------^
1111 1110
b=b^a 1111 0101
---------^
0000 1011 (十進位制數:11) b
a=a^b 1111 1110
---------^
1111 0101 (十進位制數:-11) a
```
### 按位與
按位與的運算規則是:`0^0=0` `0^1=0` `1^0=0` `1^1=1`,只有對應的位都為1的時候計算結果才是1,其他情況的計算結果都是0。主要作用是:
- 清零,如果想把一個數清零,那麼和所有位為`0`的數進行按位與即可。
- 取一個數中的指定位,例如要取`X`中的低`4`位,只需要和`zzzz...1111`進行按位與即可,例如取`1111 0110`的低`4`位,則`11110110 & 00001111`即可得到`00000110`。
### 按位或
按位與的運算規則是:`0^0=0` `0^1=1` `1^0=1` `1^1=1`,只要有其中一個位存在1則計算結果是1,只有兩個位同時為0的情況下計算結果才是0。主要作用是:
- 對一個數的部分位賦值為`1`,只需要和對應位全為`0`的數做按位或操作就行,例如`1011 0000`如果低`4`位想全部賦值為`1`,那麼`10110000 | 00001111`即可得到`1011 1111`。
### 帶符號左移
帶符號左移的運算子是`<<`,一般格式是:`M << n`。作用如下:
- `M`的二進位制數(補碼)向左移動`n`位。
- 左邊(高位)移出部分直接捨棄,右邊(低位)移入部分全部補`0`。
- 移位結果:相當於`M`的值乘以`2`的`n`次方,並且0、正、負數通用。
- 移動的位數超過了該型別的最大位數,那麼編譯器會對移動的位數取模,例如`int`移位`33`位,實際上只移動了`33 % 2 = 1`位。
推演過程如下(假設`n = 2`):
```shell
* [+ 11] 原碼 = [0000 1011] 補碼 = [0000 1011]
* [- 11] 原碼 = [1000 1011] 補碼 = [1111 0101]
* [+ 11 << 2]的計算過程
補碼 0000 1011
左移2位 0000 1011
舍高補低 0010 1100
十進位制數 2^2 + 2^3 + 2^5 = 44
* [- 11 << 2]的計算過程
補碼 1111 0101
左移2位 1111 0101
舍高補低 1101 0100
原碼 1010 1100 (補碼除最高位其他所有位取反再加1)
十進位制數 - (2^2 + 2^3 + 2^5) = -44
```
可以寫個`main`方法驗證一下:
```java
public static void main(String[] args) {
System.out.println(-11 << 2); // -44
System.out.println(11 << 2); // 44
}
```
### 組合技巧
利用上面提到的三個位運算子,相互組合可以實現一些高效的計算方案。
**計算n個bit能表示的最大數值:**
`Snowflake`演算法中有這樣的程式碼:
```java
// 機器ID的位長度
private val workerIdBits = 5L;
// 最大機器ID -> 31
private val maxWorkerId = -1L ^ (-1L << workerIdBits);
```
這裡的運算元是`-1L ^ (-1L << 5L)`,整理運算子的順序,再使用`64 bit`的二進位制數推演計算過程如下:
```shell
* [-1] 的補碼 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
左移5位 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11100000
[-1] 的補碼 11111111 11111111 11111111 11111111 11111111 11111111 11111111 11111111
異或 ----------------------------------------------------------------------- ^
結果的補碼 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00011111 (十進位制數 2^0 + 2^1 + 2^2 + 2^3 + 2^4 = 31)
```
這樣就能計算出`5 bit`能表示的最大數值`n`,`n`為整數並且`0 <= n <= 31`,即`0、1、2、3...31`。`Worker ID`和`Data Center ID`部分的最大值就是使用這種組合運算得出的。
**用固定位的最大值作為Mask避免溢位:**
`Snowflake`演算法中有這樣的程式碼:
```java
var sequence = 0L
......
private val sequenceBits = 12L
// 這裡得到的是sequence的最大值4095
private val sequenceMask = -1L ^ (-1L << sequenceBits)
......
sequence = (sequence + 1) & sequenceMask
```
最後這個運算元其實就是`sequence = (sequence + 1) & 4095`,假設`sequence`當前值為`4095`,推演一下計算過程:
```shell
* [4095] 的補碼 00000000 00000000 00000000 00000000 00000000 00000000 00000111 11111111
[sequence + 1] 的補碼 00000000 00000000 00000000 00000000 00000000 00000000 00001000 00000000
按位與 ----------------------------------------------------------------------- &
計算結果 00000000 00000000 00000000 00000000 00000000 00000000 00000000 00000000 (十進位制數:0)
```
可以編寫一個`main`方法驗證一下:
```java
public static void main(String[] args) {
int mask = 4095;
System.out.println(0 & mask); // 0
System.out.println(1 & mask); // 1
System.out.println(2 & mask); // 2
System.out.println(4095 & mask); // 4095
System.out.println(4096 & mask); // 0
System.out.println(4097 & mask); // 1
}
```
也就是`x = (x + 1) & (-1L ^ (-1L << N))`能保證最終得到的`x`值不會超過`N`,這是利用了按位與中的"取指定位"的特性。
## Snowflake演算法實現原始碼分析
`Snowflake`雖然用`Scala`語言編寫,語法其實和`Java`差不多,當成`Java`程式碼這樣閱讀就行,下面閱讀程式碼的時候會跳過一些日誌記錄和度量統計的邏輯。先看`IdWorker.scala`的屬性值:
```java
// 定義基準紀元值,這個值是北京時間2010-11-04 09:42:54,估計就是2010年初版提交程式碼時候定義的一個時間戳
val twepoch = 1288834974657L
// 初始化序列號為0
var sequence = 0L //TODO after 2.8 make this a constructor param with a default of 0
// 機器ID的最大位長度為5
private val workerIdBits = 5L
// 資料中心ID的最大位長度為5
private val datacenterIdBits = 5L
// 最大的機器ID值,十進位制數為為31
private val maxWorkerId = -1L ^ (-1L << workerIdBits)
// 最大的資料中心ID值,十進位制數為為31
private val maxDatacenterId = -1L ^ (-1L << datacenterIdBits)
// 序列號的最大位長度為12
private val sequenceBits = 12L
// 機器ID需要左移的位數12
private val workerIdShift = sequenceBits
// 資料中心ID需要左移的位數 = 12 + 5
private val datacenterIdShift = sequenceBits + workerIdBits
// 時間戳需要左移的位數 = 12 + 5 + 5
private val timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits
// 序列號的掩碼,十進位制數為4095
private val sequenceMask = -1L ^ (-1L << sequenceBits)
// 初始化上一個時間戳快照值為-1
private var lastTimestamp = -1L
// 下面的程式碼塊為引數校驗和初始化日誌列印,這裡不做分析
if (workerId > maxWorkerId || workerId < 0) {
exceptionCounter.incr(1)
throw new IllegalArgumentException("worker Id can't be greater than %d or less than 0".format(maxWorkerId))
}
if (datacenterId > maxDatacenterId || datacenterId < 0) {
exceptionCounter.incr(1)
throw new IllegalArgumentException("datacenter Id can't be greater than %d or less than 0".format(maxDatacenterId))
}
log.info("worker starting. timestamp left shift %d, datacenter id bits %d, worker id bits %d, sequence bits %d, workerid %d",
timestampLeftShift, datacenterIdBits, workerIdBits, sequenceBits, workerId)
```
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-8.png)
接著看演算法的核心程式碼邏輯:
```java
// 同步方法,其實就是protected synchronized long nextId(){ ...... }
protected[snowflake] def nextId(): Long = synchronized {
// 獲取系統時間戳(毫秒)
var timestamp = timeGen()
// 高併發場景,同一毫秒內生成多個ID
if (lastTimestamp == timestamp) {
// 確保sequence + 1之後不會溢位,最大值為4095,其實也就是保證1毫秒內最多生成4096個ID值
sequence = (sequence + 1) & sequenceMask
// 如果sequence溢位則變為0,說明1毫秒內併發生成的ID數量超過了4096個,這個時候同1毫秒的第4097個生成的ID必須等待下一毫秒
if (sequence == 0) {
// 死迴圈等待下一個毫秒值,直到比lastTimestamp大
timestamp = tilNextMillis(lastTimestamp)
}
} else {
// 低併發場景,不同毫秒中生成ID
// 不同毫秒的情況下,由於外層方法保證了timestamp大於或者小於lastTimestamp,而小於的情況是發生了時鐘回撥,下面會丟擲異常,所以不用考慮
// 也就是隻需要考慮一種情況:timestamp > lastTimestamp,也就是當前生成的ID所在的毫秒數比上一個ID大
// 所以如果時間戳部分增大,可以確定整數值一定變大,所以序列號其實可以不用計算,這裡直接賦值為0
sequence = 0
}
// 獲取到的時間戳比上一個儲存的時間戳小,說明時鐘回撥,這種情況下直接丟擲異常,拒絕生成ID
// 個人認為,這個方法應該可以提前到var timestamp = timeGen()這段程式碼之後
if (timestamp < lastTimestamp) {
exceptionCounter.incr(1)
log.error("clock is moving backwards. Rejecting requests until %d.", lastTimestamp);
throw new InvalidSystemClock("Clock moved backwards. Refusing to generate id for %d milliseconds".format(lastTimestamp - timestamp));
}
// lastTimestamp儲存當前時間戳,作為方法下次被呼叫的上一個時間戳的快照
lastTimestamp = timestamp
// 度量統計,生成的ID計數器加1
genCounter.incr()
// X = (系統時間戳 - 自定義的紀元值) 然後左移22位
// Y = (資料中心ID左移17位)
// Z = (機器ID左移12位)
// 最後ID = X | Y | Z | 計算出來的序列號sequence
((timestamp - twepoch) << timestampLeftShift) |
(datacenterId << datacenterIdShift) |
(workerId << workerIdShift) |
sequence
}
// 輔助方法:獲取系統當前的時間戳(毫秒)
protected def timeGen(): Long = System.currentTimeMillis()
// 輔助方法:獲取系統當前的時間戳(毫秒),用死迴圈保證比傳入的lastTimestamp大,也就是獲取下一個比lastTimestamp大的毫秒數
protected def tilNextMillis(lastTimestamp: Long): Long = {
var timestamp = timeGen()
while (timestamp <= lastTimestamp) {
timestamp = timeGen()
}
timestamp
}
```
最後一段邏輯的位操作比較多,但是如果熟練使用位運算操作符,其實邏輯並不複雜,這裡可以畫個圖推演一下:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-9.png)
四個部分的整數完成左移之後,由於空缺的低位都會補充了`0`,基於按位或的特性,所有低位只要存在`1`,那麼對應的位就會填充為`1`,由於四個部分的位不會越界分配,所以這裡的本質就是:**四個部分左移完畢後最終的數字進行加法計算**。
## Snowflake演算法改良
`Snowflake`演算法有幾個比較大的問題:
- 低併發場景會產生連續偶數,原因是低併發場景系統時鐘總是走到下一個毫秒值,導致序列號重置為`0`。
- 依賴系統時鐘,時鐘回撥會拒絕生成新的`ID`(直接丟擲異常)。
- `Woker ID`和`Data Center ID`的管理比較麻煩,特別是同一個服務的不同叢集節點需要保證每個節點的`Woker ID`和`Data Center ID`組合唯一。
這三個問題美團開源的`Leaf`提供瞭解決思路,下圖擷取自`com.sankuai.inf.leaf.snowflake.SnowflakeIDGenImpl`:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-7.png)
對應的解決思路是(不進行深入的原始碼分析,有興趣可以閱讀以下`Leaf`的原始碼):
- 序列號生成新增隨機源,會稍微減少同一個毫秒內能產生的最大`ID`數量。
- 時鐘回撥則進行一定期限的等待。
- 使用`Zookeeper`快取和管理`Woker ID`和`Data Center ID`。
`Woker ID`和`Data Center ID`的配置是極其重要的,對於同一個服務(例如支付服務)叢集的多個節點,必須配置不同的機器`ID`和資料中心`ID`或者同樣的資料中心`ID`和不同的機器`ID`(**簡單說就是確保`Woker ID`和`Data Center ID`的組合全域性唯一**),否則在高併發的場景下,在系統時鐘一致的情況下,很容易在多個節點產生相同的`ID`值,所以一般的部署架構如下:
![](https://throwable-blog-1256189093.cos.ap-guangzhou.myqcloud.com/202008/s-f-a-g-6.png)
管理這兩個`ID`的方式有很多種,或者像`Leaf`這樣的開源框架引入分散式快取進行管理,再如筆者所在的創業小團隊生產服務比較少,直接把`Woker ID`和`Data Center ID`硬編碼在服務啟動指令碼中,然後把所有服務使用的`Woker ID`和`Data Center ID`統一登記在團隊內部知識庫中。
## 自實現簡化版Snowflake
如果完全不考慮效能的話,也不考慮時鐘回撥、序列號生成等等問題,其實可以把`Snowflake`的位運算和異常處理部分全部去掉,使用`Long.toBinaryString()`方法結合字串按照`Snowflake`演算法思路拼接出`64 bit`的二進位制數,再通過`Long.parseLong()`方法轉化為`Long`型別。編寫一個`main`方法如下:
```java
public class Main {
private static final String HIGH = "0";
/**
* 2020-08-01 00:00:00
*/
private static final long EPOCH = 1596211200000L;
public static void main(String[] args) {
long workerId = 1L;
long dataCenterId = 1L;
long seq = 4095;
String timestampString = leftPadding(Long.toBinaryString(System.currentTimeMillis() - EPOCH), 41);
String workerIdString = leftPadding(Long.toBinaryString(workerId), 5);
String dataCenterIdString = leftPadding(Long.toBinaryString(dataCenterId), 5);
String seqString = leftPadding(Long.toBinaryString(seq), 12);
String value = HIGH + timestampString + workerIdString + dataCenterIdString + seqString;
long num = Long.parseLong(value, 2);
System.out.println(num); // 某個時刻輸出為3125927076831231
}
private static String leftPadding(String value, int maxLength) {
int diff = maxLength - value.length();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < diff; i++) {
builder.append("0");
}
builder.append(value);
return builder.toString();
}
}
```
然後把程式碼規範一下,編寫出一個簡版`Snowflake`演算法實現的工程化程式碼:
```java
// 主鍵生成器介面
public interface PrimaryKeyGenerator {
long generate();
}
// 簡易Snowflake實現
public class SimpleSnowflake implements PrimaryKeyGenerator {
private static final String HIGH = "0";
private static final long MAX_WORKER_ID = 31;
private static final long MIN_WORKER_ID = 0;
private static final long MAX_DC_ID = 31;
private static final long MIN_DC_ID = 0;
private static final long MAX_SEQUENCE = 4095;
/**
* 機器ID
*/
private final long workerId;
/**
* 資料中心ID
*/
private final long dataCenterId;
/**
* 基準紀元值
*/
private final long epoch;
private long sequence = 0L;
private long lastTimestamp = -1L;
public SimpleSnowflake(long workerId, long dataCenterId, long epoch) {
this.workerId = workerId;
this.dataCenterId = dataCenterId;
this.epoch = epoch;
checkArgs();
}
private void checkArgs() {
if (!(MIN_WORKER_ID <= workerId && workerId <= MAX_WORKER_ID)) {
throw new IllegalArgumentException("Worker id must be in [0,31]");
}
if (!(MIN_DC_ID <= dataCenterId && dataCenterId <= MAX_DC_ID)) {
throw new IllegalArgumentException("Data center id must be in [0,31]");
}
}
@Override
public synchronized long generate() {
long timestamp = System.currentTimeMillis();
// 時鐘回撥
if (timestamp < lastTimestamp) {
throw new IllegalStateException("Clock moved backwards");
}
// 同一毫秒內併發
if (lastTimestamp == timestamp) {
sequence = sequence + 1;
if (sequence == MAX_SEQUENCE) {
timestamp = untilNextMillis(lastTimestamp);
sequence = 0L;
}
} else {
// 下一毫秒重置sequence為0
sequence = 0L;
}
lastTimestamp = timestamp;
// 41位時間戳字串,不夠位數左邊補"0"
String timestampString = leftPadding(Long.toBinaryString(timestamp - epoch), 41);
// 5位機器ID字串,不夠位數左邊補"0"
String workerIdString = leftPadding(Long.toBinaryString(workerId), 5);
// 5位資料中心ID字串,不夠位數左邊補"0"
String dataCenterIdString = leftPadding(Long.toBinaryString(dataCenterId), 5);
// 12位序列號字串,不夠位數左邊補"0"
String seqString = leftPadding(Long.toBinaryString(sequence), 12);
String value = HIGH + timestampString + workerIdString + dataCenterIdString + seqString;
return Long.parseLong(value, 2);
}
private long untilNextMillis(long lastTimestamp) {
long timestamp;
do {
timestamp = System.currentTimeMillis();
} while (timestamp <= lastTimestamp);
return timestamp;
}
private static String leftPadding(String value, int maxLength) {
int diff = maxLength - value.length();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < diff; i++) {
builder.append("0");
}
builder.append(value);
return builder.toString();
}
public static void main(String[] args) {
long epoch = LocalDateTime.of(1970, 1, 1, 0, 0, 0, 0)
.toInstant(ZoneOffset.of("+8")).toEpochMilli();
PrimaryKeyGenerator generator = new SimpleSnowflake(1L, 1L, epoch);
for (int i = 0; i < 5; i++) {
System.out.println(String.format("第%s個生成的ID: %d", i + 1, generator.generate()));
}
}
}
// 某個時刻輸出如下
第1個生成的ID: 6698247966366502912
第2個生成的ID: 6698248027448152064
第3個生成的ID: 6698248032162549760
第4個生成的ID: 6698248033076908032
第5個生成的ID: 6698248033827688448
```
通過字串拼接的寫法雖然執行效率低,但是可讀性會比較高,工程化處理後的程式碼可以在例項化時候直接指定`Worker ID`和`Data Center ID`等值,並且這個簡易的`Snowflake`實現沒有第三方庫依賴,拷貝下來可以直接執行。上面的方法使用字串拼接看起來比較低端,其實最後那部分的按位或,**可以完全轉化為加法**:
```java
public class Main {
/**
* 2020-08-01 00:00:00
*/
private static final long EPOCH = 1596211200000L;
public static void main(String[] args) {
long workerId = 1L;
long dataCenterId = 1L;
long seq = 4095;
long timestampDiff = System.currentTimeMillis() - EPOCH;
long num = (long) (timestampDiff * Math.pow(2, 22)) + (long) (dataCenterId * Math.pow(2, 17)) + (long) (workerId * Math.pow(2, 12)) + seq;
System.out.println(num); // 某個時刻輸出為3248473482862591
}
}
```
這樣看起來整個演算法都變得簡單,不過這裡涉及到指數運算和加法運算,效率會比較低。
## 小結
`Snowflake`演算法是以高效能為核心目標的演算法,基於這一點目的巧妙地大量使用位運算,這篇文章已經把`Snowflake`中應用到的位運算和具體原始碼實現徹底分析清楚。最後,基於`Twitter`官方的`Snowflake`演算法原始碼,修訂出了一版`Java`實現版本,並且應用前面提到的改良方式,修復了低併發場景下只產生偶數的問題,**並且已經應用於生產環境一段時間**,程式碼倉庫如下(程式碼沒有任何第三方庫依賴,拷貝出來就直接可用):
- `Github`:`https://github.com/zjcscut/framework-mesh/tree/master/java-snowflake`
參考資料:
- [美團Leaf原始碼](https://github.com/Meituan-Dianping/Leaf)
- [Twitter Snowflake](https://github.com/twitter-archive/snowflake/blob/scala_28/src/main/scala/com/twitter/service/snowflake/IdWorker.scala)
(本文完 c-3-d e-a-20200809 封面圖來源於國漫《靈