一、雪花算法簡介
雪花算法是一種高效、分佈式全局唯一 ID 生成器,能夠生成的 ID 具有時間有序、可排序、趨勢遞增等特點,實現簡單,生成的 ID 唯一性和穩定性得到保障,廣泛應用於分佈式系統中。 在對雪花算法深入了解之前,我們先來看一下一個非常基礎的 ID 生成器。
public class IdGenerator {
private static Long id = 0L;
public static synchronized Long nextId() {
return ++id;
}
}
這個 IdGenerator 是簡單的單機 ID 生成器,通過同步保證了 ID 的唯一性。但是,這種做法有兩個問題。首先,由於 synchronized 關鍵字的使用,多線程場景下,性能會大打折扣;其次,由於是單機,可能出現重複 ID,當然這樣的概率相對較低,但是在大量生成 ID 的場景中,也無法避免。因此,需要一種分佈式的、簡單的,高效的可以提升整體系統的性能和穩定性的 ID 生成器。
二、雪花算法詳解
雪花算法的核心是一個 64 位的 long 型數字,其中,第 1 個 bit 為不用,接下來的 41 個 bit 用於表示時間戳,最高位為符號位不用,其中高 5 個 bit 是數據中心標識,5 個 bit 是機器標識,低 12 個 bit 是序號。
public class SnowflakeIdGenerator {
// 數據中心ID
private long dataCenterId;
// 機器ID
private long machineId;
// 序列號
private long sequence = 0L;
// 上一次時間戳
private long lastTimestamp = -1L;
public SnowflakeIdGenerator(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATACENTER_ID || dataCenterId MAX_MACHINE_ID || machineId < 0) {
throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_ID or less than 0");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
public synchronized long nextId() {
long currentTimestamp = timeGen();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
currentTimestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATACENTER_ID_LEFT_SHIFT)
| (machineId << MACHINE_ID_LEFT_SHIFT)
| sequence;
}
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
public static void main(String[] args) {
SnowflakeIdGenerator idWorker = new SnowflakeIdGenerator(1, 1);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
這個 SnowflakeIdGenerator 是一個典型的 Java 實現,它具有選取數據中心 ID 和機器 ID 的功能。這個算法的核心是 nextId() 方法,是一個同步方法,主要包括四個部分。
第一部分獲取當前時間戳,並檢查時間戳是否被改過。
第二部分是與上一個時間戳比較,保證沒有時間上的回撥,並計算序列號。
第三部分則是更新時間戳、序號。
第四部分則是拼裝成 64 位 long 型 ID。
三、雪花算法優化
在實際應用中,我們可能需要對雪花算法進行迭代和優化,出於不同的業務場景,也許會有不同的優化方式。這裡我們提出一些常見的優化方式。
1. 高效的防止時鐘回撥策略
有時,時鐘回撥是不可避免的。為了消除回撥帶來的影響,我們可以採用 NTP 同步系統時鐘的方式。但是在極端情況下,毫秒級別的時鐘回撥的確會發生。比如說,在雲服務器上,有時由於機器遷移或者其他原因時鐘可能會回撥,而這種回撥比較微小和短暫,但對於雪花算法而言影響卻比較大。因此,為了解決這個問題,我們需要對時鐘回撥進行優化。
public synchronized long nextId() {
long currentTimestamp = timeGen();
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset < MAX_BACKWARD_MS) {
try {
wait(offset << 1);
currentTimestamp = timeGen();
if (currentTimestamp < lastTimestamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
} catch (Exception e) {
throw new RuntimeException(e);
}
} else {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
currentTimestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATACENTER_ID_LEFT_SHIFT)
| (machineId << MACHINE_ID_LEFT_SHIFT)
| sequence;
}
可以看到,在 nextId() 方法中增加了 wait() 方法,同步等待一段時間,再次獲取當前時間戳,如果時間戳依然小於上次時間戳,則拋出異常。這種方式雖然可以滿足單數時鐘回撥帶來的影響,但是多次時鐘回撥,會導致異常被拋出。因此,我們基於這個思路,可以採取更加高效的方式求解。
public synchronized long nextId() {
long currentTimestamp = timeGen();
if (currentTimestamp < lastTimestamp) {
long offset = lastTimestamp - currentTimestamp;
if (offset < MAX_BACKWARD_MS) {
currentTimestamp = lastTimestamp;
} else {
throw new RuntimeException("Clock moved backwards. Refusing to generate id for " + offset + " milliseconds");
}
}
if (currentTimestamp == lastTimestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
currentTimestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = currentTimestamp;
return ((currentTimestamp - TWEPOCH) << TIMESTAMP_LEFT_SHIFT)
| (dataCenterId << DATACENTER_ID_LEFT_SHIFT)
| (machineId << MACHINE_ID_LEFT_SHIFT)
| sequence;
}
可以看到,只需要將等待的時間減少一半,直接使用 lastTimestamp 替代 currentTimestamp 即可,這樣的做法顯著提升了程序的性能和安全性。
2. 更為嚴格的數據安全性
在一些情況下,為了嚴格保障數據生成的唯一性,我們可能需要將 SnowflakeIdGenerator 的接口單例化,也就是說,在一個 JVM 實例上,每一個工作對象都是唯一的。
public class SnowflakeIdGenerator {
private static volatile SnowflakeIdGenerator instance;
public static SnowflakeIdGenerator getInstance(int dataCenterId, int machineId) {
if (instance == null) {
synchronized (SnowflakeIdGenerator.class) {
if (instance == null) {
instance = new SnowflakeIdGenerator(dataCenterId, machineId);
}
}
}
return instance;
}
}
這樣,一旦有重複的機器標識或者數據中心標識,就會拋出運行時異常。
四、小結
雪花算法是一種高效、分佈式全局唯一 ID 生成器,能夠生成的 ID 具有時間有序、可排序、趨勢遞增等特點,實現簡單,生成的 ID 唯一性和穩定性得到保障,廣泛應用於分佈式系統中。而在雪花算法的實現過程中,我們也可以根據不同的具體業務場景,進行一些優化和改進。
原創文章,作者:小藍,如若轉載,請註明出處:https://www.506064.com/zh-hk/n/195919.html
微信掃一掃
支付寶掃一掃