唯一ID可以標識數據的唯一性,在分佈式系統中生成唯一ID的方案有很多,常見的方式大概有以下幾種:
- 依賴數據庫,使用如MySQL自增列或Oracle序列等。
- UUID隨機數
- snowflake雪花算法(本文將要討論)
- 利用Redis 單線程處理模型的自增長實現
本文主要講解Twitter的SnowFlake(雪化算法)
數據庫和UUID方案的不足之處
採用數據庫自增序列:
- 讀寫分離時,只有主節點可以進行寫操作,可能有單點故障的風險
- 分表分庫,數據遷移合併等比較麻煩
UUID隨機數:
- 採用無意義字符串,沒有排序
- UUID使用字符串形式存儲,數據量大時查詢效率比較低
雪花算法概述
雪花算法生成的ID是純數字且具有時間順序的。其原始版本是scala版,後面出現了許多其他語言的版本如Java、C++等。
結構
- 最高位是符號位,始終爲0,不可用。
- 41位的時間序列,精確到毫秒級,41位的長度可以使用69年。時間位還有一個很重要的作用是可以根據時間進行排序。
- 10位的機器標識,10位的長度最多支持部署1024個節點
- 12位的計數序列號,序列號即一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號
特點(自增、有序、適合分佈式場景)
- 時間位:可以根據時間進行排序,有助於提高查詢速度。
- 機器id位:適用於分佈式環境下對多節點的各個節點進行標識,可以具體根據節點數和部署情況設計劃分機器位10位長度,如劃分5位表示進程位等。
- 序列號位:是一系列的自增id,可以支持同一節點同一毫秒生成多個ID序號,12位的計數序列號支持每個節點每毫秒產生4096個ID序號
優點
- 靈活配置:機器碼可以根據需求靈活配置含義
- 無需持久化:如果序號自增往往需要持久化,本算法不需要持久化
- ID 有含義/可逆性:ID 可以反解出來,對 ID 進行統計分析,可以很簡單的分析出整個系統的繁忙曲線,還可以定位到每個機器,在某段時間承擔了多少工作,分析出負載均衡情況
- 高性能:生成速度很快
雪花算法的缺點
雪花算法在單機系統上ID是遞增的,但是在分佈式系統多節點的情況下,所有節點的時鐘並不能保證不完全同步,所以有可能會出現不是全局遞增的情況。
/**
* 分佈式唯一ID 雪花算法實現
*/
public class SnowFlake {
/**
* 起始的時間戳 2020-06-30 00:00:00
*/
private final static long START_TIMESTAMP = 1593446400L;
/**
* 每一部分佔用的位數
*/
private final static long SEQUENCE_BIT = 12; //序列號佔用的位數
private final static long MACHINE_BIT = 5; //機器標識佔用的位數
private final static long DATA_CENTER_BIT = 5;//數據中心佔用的位數
/**
* 每一部分的最大值
*/
private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_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 DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
/**
* 數據中心編碼,初始化後不可修改
* 最大值: 2^5-1 取值範圍: [0,31]
*/
private long dataCenterId;
/**
* 機器或進程編碼,初始化後不可修改
* 最大值: 2^5-1 取值範圍: [0,31]
*/
private long machineId;
/**
* 序列號
* 最大值: 2^12-1 取值範圍: [0,4095]
*/
private long sequence = 0L;
private long lastStamp = -1L;//上一次時間戳
public SnowFlake(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
throw new IllegalArgumentException("dataCenterId can't be greater than MAX_DATA_CENTER_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() {
//獲取當前時間戳 可用System.currentTimeMillis(); 方法替換
long currStamp = getNewStamp();
//保證當前時間戳不小於最後一次的時間戳
if (currStamp < lastStamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
//如果當前時間戳等於最後一次時間戳 就進行序號遞增
if (currStamp == lastStamp) {
//相同毫秒內,序列號自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列數已經達到最大
if (sequence == 0L) {
currStamp = getNextMill();
}
} else {
//不同毫秒內,序列號置爲0
sequence = 0L;
}
//保存最後一次時間戳
lastStamp = currStamp;
//通過位運算,將相應的二進制數值放到對應的位置 41位時間戳 + 5位數據中心 + 5位機器碼 + 12位序列號 ,首位沒有使用
return (currStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //時間戳部分
| dataCenterId << DATA_CENTER_LEFT //數據中心部分
| machineId << MACHINE_LEFT //機器標識部分
| sequence; //序列號部分
}
private long getNextMill() {
//獲取當前時間戳
long mill = getNewStamp();
//獲取大於上一次時間戳的時間戳
while (mill <= lastStamp) {
mill = getNewStamp();
}
return mill;
}
/**
* 獲取當前時間戳
*/
private long getNewStamp() {
return System.currentTimeMillis();
}
//測試
public static void main(String[] args) {
SnowFlake snowFlake = new SnowFlake(2, 3);
long start = System.currentTimeMillis();
for (int i = 0; i < 100000; i++) {
System.out.println(snowFlake.nextId());
}
}
}
總結
分佈式唯一ID的方案有很多,本文主要討論了雪花算法,組成結構大致分爲了無效位、時間位、機器位和序列號位。其特點是自增、有序、純數字組成查詢效率高且不依賴於數據庫。適合在分佈式的場景中應用,可根據需求調整具體實現細節。對於瞬時的高峯唯一ID請求,可以考慮結合Redis 隊列進行預生產大量ID,業務消費方優先從Redis獲取,ID生產方 持續寫入到Redis進行瞬時問題處理。