Snowflake is a service used to generate unique IDs for objects within Twitter (Tweets, Direct Messages, Users, Collections, Lists etc.). These IDs are unique 64-bit unsigned integers, which are based on time, instead of being sequential. The full ID is composed of a timestamp, a worker number, and a sequence number. When consuming the API using JSON, it is important to always use the field id_str instead of id. This is due to the way Javascript and other languages that consume JSON evaluate large integers. If you come across a scenario where it doesn’t appear that id and id_str match, it’s due to your environment having already parsed the id integer, munging the number in the process. —— developer.twitter.com
Snowflake(雪花) 是一項服務,用於爲 Twitter 內的對象(推文,直接消息,用戶,集合,列表等)生成惟一的 ID。這些 IDs 是惟一的 64 位無符號整數,它們基於時間,而不是順序的。完整的 ID 由時間戳,工做機器編號和序列號組成。當在 API 中使用 JSON 數據格式時,請務必始終使用 id_str 字段而不是 id,這一點很重要。這是因爲處理JSON 的 Javascript 和其餘語言計算大整數的方式形成的。若是你遇到 id 和 id_str 彷佛不匹配的狀況,這是由於你的環境已經解析了 id 整數,並在處理的過程當中仔細分析了這個數字。html
在 JavaScript 中,Number
基本類型能夠精確表示的最大整數是 2^53。所以若是直接使用 Number 來表示 64 位的 Snowflake ID 確定是行不通的。因此 Twitter 工程師們讓咱們務必使用 id_str 字段即經過字符串來表示生成的 ID。固然這個問題不只僅存在於使用 Snowflake ID 的場景,爲了解決 JavaScript 不能安全存儲和操做大整數的問題,BigInt
這個救星出現了,它是一種內置對象,能夠表示大於 2^53 的整數,甚至是任意大的整數。java
BigInt 如今處在 ECMAScript 標準化過程當中的 第三階段 。當它進入第四階段草案,也就是最終標準時, BigInt 將成爲 Javacript 中的第二種內置數值類型。mysql
BigInt
可能會成爲自 ES2015 引入Symbol
以後,增長的第一個新的內置類型。git
下圖是 Snowflake 算法的 ID 構成圖:github
Snowflake 算法生成的 ID 大體上是按照時間遞增的,用在分佈式系統中時,須要注意數據中心標識和機器標識必須惟一,這樣就能保證每一個節點生成的 ID 都是惟一的。咱們不必定須要像 Twitter 那樣使用 5 位做爲數據中心標識,另 5 位做爲機器標識,能夠根據咱們業務的須要,靈活分配工做機器 ID 部分。好比:若不須要數據中心,徹底可使用所有 10 位做爲機器標識;若數據中心很少,也能夠只使用 3 位做爲數據中心,7 位做爲機器標識。算法
如下問題來源於漫漫路博客 - 「Twitter-Snowflake,64 位自增ID算法詳解」 評論區
首位不用主要是爲了保持 ID 的自增特性,若使用了最高位,int64_t 會表示爲負數。在 Java 中因爲 long 類型的最高位是符號位,正數是 0,負數是 1,通常生成的 ID 爲正整數,因此最高位爲 0。sql
41 位的時間戳,這個是毫秒級的時間,通常實現上不會存儲當前的時間戳,而是時間戳的差值(當前時間減去固定的開始時間)。41 位只是預留位(主要目的是約定使用年限,固定的開始時間),不用的位數填 0 就行了。docker
網絡中每臺設備都有一個惟一的網絡標識,這個地址叫 MAC 地址或網卡地址,由網絡設備製造商生產時寫在硬件內部。MAC 地址則是 48 位的(6 個字節),一般表示爲 12 個 16 進制數,每 2 個 16 進制數之間用冒號隔開,如08:00:20:0A:8C:6D 就是一個 MAC 地址。數據庫
具體以下圖所示,其前 3 字節表示OUI(Organizationally Unique Identifier),是 IEEE (電氣和電子工程師協會)區分不一樣的廠家,後 3 字節由廠家自行分配。緩存
(圖片來源 - 百度百科)
很明顯 Mac 地址是 48 位,而咱們的工做機器 ID 部分只有 10 位,所以並不能直接使用 Mac 地址做爲工做機器 ID。若要選用 Mac 地址的話,還需使用一個額外的工做機器 ID 分配器,用來實現 ID 與 Mac 地址間的惟一映射。
序列號不須要全局維護,在 Java 中可使用 AtomicInteger(保證線程安全) 從 0 開始自增。當序列號超過了 4096,序列號在這一毫秒就用完了,等待下一個毫秒歸 0 重置就能夠了。
理論上 Snowflake 方案的 QPS 約爲 409.6w/s(1000 * 2^12),這種分配方式能夠保證在任何一個 IDC 的任何一臺機器在任意毫秒內生成的 ID 都是不一樣的。
除了時鐘回撥問題以外,Snowflake 算法會存在併發限制,固然對於這些問題,以本人目前的 Java 功力根本解決不了。但這並不影響咱們使用它。在實際項目中咱們可使用基於 Snowflake 算法的開源項目,好比百度的 UidGenerator 或美團的 Leaf。下面咱們簡單介紹一下這兩個項目,感興趣的小夥伴能夠自行查閱相關資料。
UidGenerator 是 Java 實現的,基於 Snowflake 算法的惟一 ID 生成器。UidGenerator 以組件形式工做在應用項目中,支持自定義 workerId 位數和初始化策略,從而適用於 docker 等虛擬化環境下實例自動重啓、漂移等場景。 在實現上,UidGenerator 經過借用將來時間來解決 sequence 自然存在的併發限制;採用 RingBuffer 來緩存已生成的 UID,並行化 UID 的生產和消費,同時對 CacheLine 補齊,避免了由 RingBuffer 帶來的硬件級「僞共享」問題。最終單機 QPS 可達 600 萬。
依賴版本:Java8 及以上版本,MySQL (內置 WorkerID 分配器,啓動階段經過 DB 進行分配;如自定義實現,則 DB非必選依賴)。
Leaf 最先期需求是各個業務線的訂單 ID 生成需求。在美團早期,有的業務直接經過 DB 自增的方式生成 ID,有的業務經過 Redis 緩存來生成 ID,也有的業務直接用 UUID 這種方式來生成 ID。以上的方式各自有各自的問題,所以咱們決定實現一套分佈式 ID 生成服務來知足需求。具體 Leaf 設計文檔見: Leaf 美團分佈式ID生成服務。
目前 Leaf 覆蓋了美團點評公司內部金融、餐飲、外賣、酒店旅遊、貓眼電影等衆多業務線。在 4C8G VM 基礎上,經過公司 RPC 方式調用,QPS 壓測結果近5w/s,TP999 1ms。
在前面 Snowflake 知識的基礎上,如今咱們來分析一下 Github 上 beyondfengyu 大佬基於 Java 實現的 SnowFlake,完整代碼以下:
/** * twitter的snowflake算法 -- java實現 * * @author beyond * @date 2016/11/26 */ public class SnowFlake { /** * 起始的時間戳 */ private final static long START_STMP = 1480166465631L; /** * 每一部分佔用的位數 */ private final static long SEQUENCE_BIT = 12; //序列號佔用的位數 private final static long MACHINE_BIT = 5; //機器標識佔用的位數 private final static long DATACENTER_BIT = 5;//數據中心佔用的位數 /** * 每一部分的最大值 */ private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); 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 long datacenterId; //數據中心 private long machineId; //機器標識 private long sequence = 0L; //序列號 private long lastStmp = -1L;//上一次時間戳 public SnowFlake(long datacenterId, long machineId) { if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException( "datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException( "machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; } /** * 產生下一個ID * * @return */ public 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; } lastStmp = currStmp; return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分 | datacenterId << DATACENTER_LEFT //數據中心部分 | machineId << MACHINE_LEFT //機器標識部分 | sequence; //序列號部分 } private long getNextMill() { long mill = getNewstmp(); while (mill <= lastStmp) { mill = getNewstmp(); } return mill; } private long getNewstmp() { return System.currentTimeMillis(); } public static void main(String[] args) { SnowFlake snowFlake = new SnowFlake(2, 3); for (int i = 0; i < (1 << 12); i++) { System.out.println(snowFlake.nextId()); } } }
在詳細分析以前,咱們先來回顧一下 Snowflake 算法的 ID 構成圖:
首位不用,默認爲 0。41bit(第2-42位)時間戳,是相對時間戳,經過當前時間戳減去一個固定的歷史時間戳生成。在 SnowFlake 類定義了一個 long 類型的靜態變量 START_STMP,它的值爲 1480166465631L:
/** * 起始的時間戳:Sat Nov 26 2016 21:21:05 GMT+0800 (中國標準時間) */ private final static long START_STMP = 1480166465631L;
接着繼續定義三個 long 類型的靜態變量,來表示序列號和工做機器 ID 的佔用位數:
/** * 每一部分佔用的位數 */ private final static long SEQUENCE_BIT = 12; //序列號佔用的位數 private final static long MACHINE_BIT = 5; //機器標識佔用的位數 private final static long DATACENTER_BIT = 5;//數據中心佔用的位數
此外還定義了每一部分的最大值,具體以下:
/** * 每一部分的最大值 */ private final static long MAX_DATACENTER_NUM = -1L ^ (-1L << DATACENTER_BIT); // 31 private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT); // 31 private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT); // 4095
SnowFlake 類的構造函數,該構造函數含有 datacenterId 和 machineId 兩個參數,它們分別表示數據中心 id 和機器標識:
private long datacenterId; //數據中心 private long machineId; //機器標識 public SnowFlake(long datacenterId, long machineId) { if (datacenterId > MAX_DATACENTER_NUM || datacenterId < 0) { throw new IllegalArgumentException( "datacenterId can't be greater than MAX_DATACENTER_NUM or less than 0"); } if (machineId > MAX_MACHINE_NUM || machineId < 0) { throw new IllegalArgumentException( "machineId can't be greater than MAX_MACHINE_NUM or less than 0"); } this.datacenterId = datacenterId; this.machineId = machineId; }
在 SnowFlake 類的實現中,在建立完 SnowFlake 對象以後,能夠經過調用 nextId
方法來獲取 ID。有的小夥伴可能對位運算不太清楚,這裏先簡單介紹一下 nextId
方法中,所用到的位運算知識。
按位與運算符(&)
參加運算的兩個數據,按二進制位進行 「與」 運算,它的運算規則:
0&0=0; 0&1=0; 1&0=0; 1&1=1;
即兩位同時爲 1,結果才爲 1,不然爲 0。
按位或運算(|)
參加運算的兩個對象,按二進制位進行 「或」 運算,它的運算規則:
0|0=0; 0|1=1; 1|0=1; 1|1=1;
即僅當兩位都爲 0 時,結果才爲 0。
左移運算符 <<
將一個運算對象的各二進制位所有左移若干位(左邊的二進制位丟棄,右邊補 0)。若左移時捨棄的高位不包含1,則每左移一位,至關於該數乘以 2。
在瞭解完位運算的相關知識後,咱們再來看一下 nextId 方法的具體實現:
/** * 產生下一個ID * * @return */ public synchronized long nextId() { // 獲取當前的毫秒數:System.currentTimeMillis(),該方法產生一個當前的毫秒,這個毫秒 // 其實就是自1970年1月1日0時起的毫秒數。 long currStmp = getNewstmp(); // private long lastTimeStamp = -1L; 表示上一次時間戳 // 檢測是否出現時鐘回撥 if (currStmp < lastStmp) { throw new RuntimeException("Clock moved backwards. Refusing to generate id"); } // 相同毫秒內,序列號自增 if (currStmp == lastStmp) { // private long sequence = 0L; 序列號 // MAX_SEQUENCE = 4095 111111111111 // MAX_SEQUENCE + 1 = 4096 1000000000000 sequence = (sequence + 1) & MAX_SEQUENCE; // 同一毫秒的序列數已經達到最大 if (sequence == 0L) { // 阻塞到下一個毫秒,得到新的時間戳 currStmp = getNextMill(); } } else { //不一樣毫秒內,序列號置爲0 sequence = 0L; } lastStmp = currStmp; // MACHINE_LEFT = SEQUENCE_BIT; -> 12 // DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT; -> 17 // TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT; -> 22 return (currStmp - START_STMP) << TIMESTMP_LEFT //時間戳部分 | datacenterId << DATACENTER_LEFT //數據中心部分 | machineId << MACHINE_LEFT //機器標識部分 | sequence; //序列號部分 }
如今咱們來看一下使用方式:
public static void main(String[] args) { SnowFlake snowFlake = new SnowFlake(2, 3); for (int i = 0; i < (1 << 12); i++) { System.out.println(snowFlake.nextId()); } }
如今咱們已經能夠利用 SnowFlake 對象生成惟一 ID 了,那這個惟一 ID 有什麼用呢?這裏舉一個簡單的應用場景,即基於 SnowFlake 的短網址生成器,其主要思路是使用 SnowFlake 算法生成一個整數,而後對該整數進行 62 進制編碼最終生成一個短地址 URL。對短網址生成器感興趣的小夥伴,能夠參考 徐劉根 大佬在碼雲上分享的工具類。
最後咱們來簡單總結一下,本文主要介紹了什麼是 Snowflake(雪花)算法、Snowflake 算法 ID 構成圖及其優缺點,最後詳細分析了 Github 上 beyondfengyu 大佬基於 Java 實現的 SnowFlake。在實際項目中,建議你們選用基於 Snowflake 算法成熟的開源項目,如百度的 UidGenerator 或美團的 Leaf。