一、概述
分散式 ID 生成演算法的有很多種,Twitter 的 SnowFlake 就是其中經典的一種。
SnowFlake 演算法生成 ID 的結果是一個 64bit 大小的整數,它的結構如下圖:
- 1 位,不用。二進位制中最高位為 1 的都是負數,但是我們生成的 id 一般都使用整數,所以這個最高位固定是 0。
- 41 位,用來記錄時間戳(毫秒)。41 位可以表示 2^41 個數字;如果只用來表示正整數(計算機中正數包含 0),可以表示的數值範圍是:0 至 2^41−1,也就是說 41 位可以表示 2^41 個毫秒的值,轉化成單位年則是 2^41 / (1000 * 60 * 60 * 24 * 365) = 69年。
- 10 位,用來記錄工作機器 id,可以部署在 2^10 = 1024個節點,包括 5 位 datacenterId 和 5 位 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、....4095 這 4096 個數字,來表示同一機器同一時間截(毫秒)內產生的 4096 個ID序號。
由於在 Java 中 64 bit 的整數是 Long 型別,所以在 Java 中 SnowFlake 演算法生成的 ID 就是 Long 來儲存的。
SnowFlake 可以保證:
- 所有生成的 ID 按時間趨勢遞增;
- 整個分散式系統內不會產生重複id(因為有 datacenterId 和 workerId 來做區分);
二、SnowFlake 演算法的 JAVA 實現
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 << DATACENTER_BIT);
private final static long MAX_MACHINE_NUM = ~(-1L << MACHINE_BIT);
private final static long MAX_SEQUENCE = ~(-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);
long start = System.currentTimeMillis();
for (int i = 0; i < 1000; i++) {
System.out.println(snowFlake.nextId());
}
System.out.println(System.currentTimeMillis() - start);
}
}
三、SnowFlake 演算法的生產實踐
通過前面的講述我們知道 SnowFlake 演算法能產生全域性唯一 ID 主要是通過 5 位 datacenterId 和 5 位 workerId 來做區分,而在生產實踐中,我們常常將機器 ID(hostid)來當成 datacenterId,將機器上執行的例項ID(pid)來當成 workerId。
如今的分散式部署都在強調無狀態化,那麼給每臺機器繫結一個 hostid 顯然不太現實,假設又是在容器化環境下,沒有固定的 IP,並且容器例項每次重新啟動後的唯一 ID 還不一致。綜上,基於機器 ID 的思路不可行。所以經常的做法是,利用 ZK/Redis/DB 等作為一個全域性的 datacenterId/workerId 發號器,由發號器來分配唯一的 datacenterId/workerId 。(可以參考百度分散式唯一 ID 生成器 UidGenerator)
四、SnowFlake 演算法的問題思考
1. 時間回撥問題
由於機器的時間是動態的調整的,有可能會出現時間跑到之前幾毫秒,如果這個時候獲取到了這種時間,則會出現資料重複。
這個問題的解決方案是採用“歷史時間”。在程式啟動後,我們會將當前時間(實際處理採用了延遲10ms啟動),作為該業務這臺機器程式的時間戳中的起始時間欄位,後續的自增是在序列號自增到最大值時,時間戳增 1,而序列號重新歸為 0。
2. 機器 id 分配及回收
目前機器 id 需要每臺機器不一樣,這樣的方式分配需要有方案進行處理,同時也要考慮,如果該機器當機了,對應的 datacenterId/workerId 分配後的回收問題。
這個問題的解決方案是:每個例項啟動,擴容,直接從 ZK/Redis/DB 等發號器取一個 id 作為 datacenterId/workerId,下線不銷燬;並且維護一個活動節點佇列,在地址空間耗盡的時候,指標指回佇列頭部,當分配的 id 存在於活動節點佇列則跳過取下一個可用空間,達到複用原地址空間的目的。