分佈式ID: 雪花算法(snowflake)
1、算法概述
- 有這麼一種說法,自然界中並不存在兩片完全一樣的雪花的,每一片雪花都擁有自己漂亮獨特的形狀、獨一無二。雪花算法也表示生成的ID如雪花般獨一無二。
- 當我們需要無序不能被猜測的ID,並且需要一定高性能,那麼就可以使用雪花算法。比如常見的訂單ID,用雪花算法別人就無法猜測你每天的訂單量是多少。
- SnowFlake雪花算法,是Twitter開源的分佈式ID生成算法。其核心思想就是:使用一個64 bit的long型的數字作爲全局唯一ID。
2、組成結構
3、算法的特點
段位 | 位數bit | 描述 |
---|---|---|
最高位 | 1 | 始終爲0,是符號位,不可用 |
時間位 | 41 | 精確到毫秒級,41位的長度可以使用69年,時間位還有一個很重要的作用是可以根據時間進行排序 |
機器標識 | 10 | 10位的長度最多支持部署1024個節點,適用於分佈式環境下對多節點的各個節點進行標識 |
序列號 | 12 | 12位的長度支持每毫秒產生4096個ID序號 |
上面只是一個將64bit劃分的標準,其中10bit機器id和12bit的序列號不一定要這麼做,可以根據實際情況和需求來劃分。
4、算法的實現Java
/**
* @author administrator
* @date 2020-05-05 13:41
*/
/**
* Twitter_Snowflake<br>
* SnowFlake的結構如下(每部分用-分開):<br>
* 0 - 0000000000 0000000000 0000000000 0000000000 0 - 00000 - 00000 - 000000000000 <br>
* 1位標識,由於long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0<br>
* 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)
* 得到的值),這裏的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的(如下下面程序的startTime屬性)。
* 41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69<br>
* 10位的數據機器位,可以部署在1024個節點,本程序使用5位datacenterId和5位workerId<br>
* 12位序列,毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號<br>
* 加起來剛好64位,爲一個Long型。<br>
* SnowFlake的優點是,整體上按照時間自增排序,並且整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分),並且效率較高
*/
public class SnowFlakeGenerator {
// ==============================Fields===========================================
/**
* 開始時間截 (2020-05-05)
*/
private final long startTime = 1588670157222L;
/**
* 機器ID所佔的位數
*/
private final long workerIdBits = 5L;
/**
* 數據中心ID所佔的位數
*/
private final long datacenterIdBits = 5L;
/**
* 支持的最大機器ID,結果是31 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數)
*/
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/**
* 支持的最大數據中心ID,結果是31
*/
private final long maxDatacenterId = -1L ^ (-1L << datacenterIdBits);
/**
* 序列在id中佔的位數
*/
private final long sequenceBits = 12L;
/**
* 機器ID向左移12位
*/
private final long workerIdShift = sequenceBits;
/**
* 數據中心ID向左移17位(5+12)
*/
private final long datacenterIdShift = workerIdBits + sequenceBits;
/**
* 時間截向左移22位(5+5+12)
*/
private final long timestampLeftShift = datacenterIdBits + workerIdBits + sequenceBits;
/**
* 生成序列的掩碼,這裏爲4095
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作機器ID(0~31)
*/
private long workerId;
/**
* 數據中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒內序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的時間截
*/
private long lastTimestamp = -1L;
//==============================Constructors=====================================
/**
* 構造函數
*
* @param datacenterId 數據中心ID (0~31)
* @param workerId 工作ID (0~31)
*/
public SnowFlakeGenerator(long datacenterId, long workerId) {
if (datacenterId > maxDatacenterId || datacenterId < 0) {
throw new IllegalArgumentException(String.format("datacenterId can't be greater than %d or less than 0", maxDatacenterId));
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("workerId can't be greater than %d or less than 0", maxWorkerId));
}
this.datacenterId = datacenterId;
this.workerId = workerId;
}
// ==============================Methods==========================================
/**
* 獲得下一個ID (該方法是線程安全的)
*
* @return SnowflakeId
*/
public synchronized long nextId() {
long timestamp = timeGen();
//如果當前時間小於上一次ID生成的時間戳,說明系統出現了時鐘回退,這個時候應當拋出異常
if (timestamp < lastTimestamp) {
throw new RuntimeException(String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp));
}
if (lastTimestamp == timestamp) {
//最後一次生成ID的時間(毫秒)與當前時間相同,則進行毫秒內序列+1
sequence = (sequence + 1) & sequenceMask;
//當某一毫秒的時間,產生的id數超過4095,系統會進入等待,直到下一毫秒,系統繼續產生ID
if (sequence == 0L) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
} else {
//時間戳改變,毫秒內序列重置
sequence = 0L;
}
//上次生成ID的時間截
lastTimestamp = timestamp;
//移位並通過或運算拼到一起組成64位的ID
return ((timestamp - startTime) << timestampLeftShift)
| (datacenterId << datacenterIdShift)
| (workerId << workerIdShift)
| sequence;
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
*
* @param lastTimestamp 上次生成ID的時間截
* @return 當前時間戳
*/
private long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒爲單位的當前時間
*
* @return 當前時間(毫秒)
*/
private long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/**
* 測試
*/
public static void main(String[] args) {
SnowFlakeGenerator idWorker = new SnowFlakeGenerator(1, 1);
for (int i = 0; i < 1000; i++) {
long id = idWorker.nextId();
System.out.println(Long.toBinaryString(id));
System.out.println(id);
}
}
}
5、算法的缺點
-
雪花算法在單機系統上ID是遞增的,但是在分佈式系統多節點的情況下,所有節點的時鐘並不能保證不完全同步,所以有可能會出現不是全局遞增的情況。
-
依賴與系統時間的一致性,如果系統時間被回調,或者改變,可能會造成id衝突或者重複。