目錄
SnowFlake算法,是Twitter開源的分佈式ID生成算法。其核心思想就是使用使用一個64爲的Long類型的數據作爲全局唯一ID。
1、基本原理
我們都知道計算機語言是2進制語言,則計算機存放數字都是存放數字的補碼。
例如:-1(一般一個byte爲8位,-1L就是64個1)的補碼是1111 1111 反碼是1111 1110 原碼是1000 0001。
另外在計算中通常把最高位當成是符號位,“0”:表示負數,“1”:表示正數
^ 表示異或 即:相同爲0,相異爲1; << 左移運算符 即:-1L<<5L 表示1110 0000;
例如:(-1L^(-1L<<5L))
表示:1111 1111^1110 0000
結果:0001 1111
利用8421碼進行轉換 128 64 32 16 8 4 2 1 轉換爲十進制爲16+8+4+2+1=31
2、SnowFlake的結構如下(每部分用-分開):
0 - 0000000000 0000000000 0000000000 0000000000 0000000000 0 – 0000000000 00 – 00000000 0000
1位標識,由於long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0
41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截)得到的值,這裏的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的(如下下面程序IdWorker類的startTime屬性)。41位的時間截,可以使用69年,T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
10位的數據機器位,可以部署在1024個節點,包括5位datacenterId和5位workerId,這個位數是可以修改的
12位序列,毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號
以上加起來剛好64位,爲一個Long型。
3、優缺點
優點:整體上按照時間自增排序,並且整個分佈式系統內不會產生ID碰撞(由數據中心ID和機器ID作區分),並且效率較高
缺點:如果節點上的時間倒退可能會出現重複ID的情況。
4、算法實現
public class GeneratorIdUtil {
private static Logger logger = LoggerFactory.getLogger(GeneratorIdUtil.class);
/**
* 開始時間截,這裏設置爲項目開始的時間2018-05-24 從2018-05-24算起大約能夠使用69年
*/
private final long twepoch = 1527150129903L;
/**
* 機器id所佔的位數,這裏設置位10L,因爲datacenterIdBits不需要
* 如果workerIdBits=5L 則datacenterIdBits=5L
*/
private final long workerIdBits = 10L;
/*
* 數據標識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位(12+5)
*/
// private final long datacenterIdShift = sequenceBits + workerIdBits;
/**
* 時間截向左移22位(5+5+12)
* //+ datacenterIdBits;
*/
private final long timestampLeftShift = sequenceBits + workerIdBits;
/**
* 生成序列的掩碼,這裏爲4095 (0b111111111111=0xfff=4095)
*/
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/**
* 工作機器ID(0~1023)
*/
private long workerId;
/**
* 數據中心ID(0~31)
*/
private long datacenterId;
/**
* 毫秒內序列(0~4095)
*/
private long sequence = 0L;
/**
* 上次生成ID的時間截
*/
private long lastTimestamp = -1L;
/**
* 構造函數
*
* @param workerId 工作ID (0~1023)
*/
private GeneratorIdUtil(long workerId) {
logger.info("workerId : {}", workerId);
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
this.workerId = workerId;
}
/**
* 構造函數
*/
private GeneratorIdUtil() {
this.workerId = getIdWorker();
logger.info("workerId : {}", workerId);
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
}
/**
* 獲得下一個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) {
sequence = (sequence + 1) & sequenceMask;
//毫秒內序列溢出
if (sequence == 0) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
} else { //時間戳改變,毫秒內序列重置
sequence = 0L;
}
//上次生成ID的時間截
lastTimestamp = timestamp;
//移位並通過或運算拼到一起組成64位的ID
return ((timestamp - twepoch) << 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();
}
/**
* 獲取機器編號,設置機器編號的方式有很多,這裏根據業務需求取IP地址的後三位
* 根據前面分析該ID位0-1023
* param
*/
private int getIdWorker() {
InetAddress addr = null;
String ip = "";
try {
addr = InetAddress.getLocalHost();
/*獲取本機IP*/
ip = addr.getHostAddress();
logger.info("本機IP : {} ", ip);
return Integer.parseInt(ip.split("\\.")[3]);
} catch (Exception e) {
logger.error("獲取機器編號錯誤 : {} ", ip + e);
}
return 0;
}
}
5、測試
這裏只做簡單的測試,不考慮數據庫性能。
測試電腦配置如下
- 機器ID爲1023,測試代碼如下:
@SneakyThrows
public static void main(String[] args) {
long startTime = System.currentTimeMillis() / 1000;
GeneratorIdUtil generatorIdUtil = new GeneratorIdUtil(1023);
int count = 1;
CountDownLatch countDownLatch = new CountDownLatch(count);
for (int i = 0; i < count; i++) {
ThreadPoolUtils.getThreadPool().execute(() -> {
try {
for (int k = 0; k < 1000000; k++) {
generatorIdUtil.nextId();
}
} finally {
countDownLatch.countDown();
}
}
);
}
ThreadPoolUtils.shutdown();
countDownLatch.await();
long endTime = System.currentTimeMillis() / 1000;
logger.info("目標執行時間 : {} s", (endTime - startTime));
}
public class ThreadPoolUtils {
private static final ExecutorService threadPool;
CountDownLatch countDownLatch;
static{
ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
threadPool = new ThreadPoolExecutor(20, 20, 50,
TimeUnit.SECONDS, new ArrayBlockingQueue<>(1),
threadFactory, new ThreadPoolExecutor.AbortPolicy());
}
public static ExecutorService getThreadPool() {
return threadPool;
}
public static void shutdown() {
threadPool.shutdown();
}
}
測試結果
線程數 |
量級 |
耗時 |
1 |
10000000 |
2~3s |
1 |
1000000 |
3ms |
10 |
1000000 |
2~3s |
10 |
10000000 |
25s |
歡迎掃碼關注公衆號