分布式全局ID生成器設計
項目是分布式的架構,需要設計一款分布式全局ID,參照了多種方案,最后基於snowflake的算法設計了一款自用ID生成器。具有以下優勢:
- 保證分布式場景下生成的ID是全局唯一的
- 生成的全局ID整體上是呈自增趨勢的,也就是說整體是粗略有序的
- 高性能,能快速產生ID,本機(I7-6400HQ)單線程可以達到每秒生成近40萬個ID
- 只占64bit位空間,可以根據業務需求擴展在前綴或后綴拼接業務標志位轉化為字符串。
UUID方案
- UUID:UUID長度128bit,32個16進制字符,占用存儲空間多,且生成的ID是無序的
- 對於InnoDB這種聚集主鍵類型的引擎來說,數據會按照主鍵進行排序,由於UUID的無序性,InnoDB會產生巨大的IO壓力,此時不適合使用UUID做物理主鍵,可以把它作為邏輯主鍵,物理主鍵依然使用自增ID。
- 組成部分:當前日期和時間、時鍾序列、機器識別碼
數據庫生成全局ID方案
- 結合數據庫維護一個Sequence表,每當需要為某個表的新紀錄生成ID時就從Sequence表中取出對應的nextid,將其+1后更新到數據庫中以備下次使用。
- 由於所有的插入都要訪問該表,很容易造成性能瓶頸,且存在單點問題,如果該表所在的數據庫失效,全部應用無法工作。
- 在高並發場景下,無法保證高性能。
snowflake方案
是一個優秀的分布式Id生成方案,是Scala實現的,此次項目就是基於snowflake算法基礎上設計的Java優化版
- 1位,不用。二進制中最高位為1的都是負數,但是我們生成的id一般都使用整數,所以這個最高位固定是0
- 41位,用來記錄時間戳(毫秒),41位可以表示241−1個數字,也就是說41位可以表示241−1個毫秒的值,轉化成單位年則是(2^41−1)/(1000∗60∗60∗24∗365)=69年
- 10位,用來記錄工作機器id。可以部署在210=1024個節點,包括5位datacenterId和5位workerId,5位(bit)可以表示的最最大正整數是2^5−1=31,即可以用0、1、2、3、....31這32個數字,來表示不同的datecenterId或workerId
- 12位,序列號,用來記錄同毫秒內產生的不同id。12位(bit)可以表示的最大正整數是212−1=4095,即可以用0、1、2、3、....4095這4096個數字,來表示同一機器同一時間截(毫秒)內產生的4096個ID序號
改進方案
全局唯一ID生成結構如下(每部分用-分開):
- 0 - 00 - 0000000000 0000000000 0000000000 0000000000 0 - 0000000000 00 - 00000000
- 1位標識,由於long基本類型在Java中是帶符號的,最高位是符號位,正數是0,負數是1,所以id一般是正數,最高位是0
- 2位生成發布方式,0代表嵌入式發布、1代表中心服務器發布模式、2代表rest發布方式、3代表測試方式
- 41位時間截(毫秒級),注意,41位時間截不是存儲當前時間的時間截,而是存儲時間截的差值(當前時間截 - 開始時間截得到的值),這里的的開始時間截,一般是我們的id生成器開始使用的時間,由我們程序來指定的。41位的時間截,可以使用69年,年T = (1L << 41) / (1000L * 60 * 60 * 24 * 365) = 69
- 12位序列,毫秒內的計數,12位的計數順序號支持每個節點每毫秒(同一機器,同一時間截)產生4096個ID序號
- 8位的數據機器位,可以部署在256個節點,包括8位workerId
- 加起來剛好64位,為一個Long型
- 優點是,整體上按照時間自增排序,並且整個分布式系統內不會產生ID碰撞(機器ID作區分),並且效率較高,經本地測試每秒能夠產生40萬ID左右。
方案優勢
- 保證分布式場景下生成的ID是全局唯一的
- 生成的全局ID整體上是呈自增趨勢的,也就是說整體是粗略有序的
- 高性能,能快速產生ID,本機單線程可以達到每秒生成近40萬個ID
- 只占64bit位空間,可以根據業務需求在前綴或后綴拼接業務標志位。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public final class IdGenerate {
// ==============================Fields===========================================
/** 開始時間截 (2018-01-01) */
private final long twepoch = 1514736000000L;
/** 機器id所占的位數 */
private final long workerIdBits = 8L;
/** 序列在id中占的位數 */
private final long sequenceBits = 12L;
/** 毫秒級別時間截占的位數 */
private final long timestampBits = 41L;
/** 生成發布方式所占的位數 */
private final long getMethodBits = 2L;
/** 支持的最大機器id,結果是255 (這個移位算法可以很快的計算出幾位二進制數所能表示的最大十進制數) */
private final long maxWorkerId = -1L ^ (-1L << workerIdBits);
/** 生成序列向左移8位(8) */
private final long sequenceShift = workerIdBits;
/** 時間截向左移20位(12+8) */
private final long timestampShift = sequenceBits + workerIdBits;
/** 生成發布方式向左移61位(41+12+8) */
private final long getMethodShift = timestampBits + sequenceBits + workerIdBits;
/** 工作機器ID(0~255) */
private long workerId = 0L;
/** 生成序列的掩碼,這里為4095 (0b111111111111=0xfff=4095) */
private final long sequenceMask = -1L ^ (-1L << sequenceBits);
/** 毫秒內序列(0~4095) */
private long sequence = 0L;
/** 上次生成ID的時間截 */
private long lastTimestamp = -1L;
/** 2位生成發布方式,0代表嵌入式發布、1代表中心服務器發布模式、2代表rest發布方式、3代表保留未用 */
private long getMethod = 0L;
/** 成發布方式的掩碼,這里為3 (0b11=0x3=3) */
private long maxGetMethod = -1L ^ (-1L << getMethodBits);
/** 重入鎖*/
private Lock lock = new ReentrantLock();
//==============================Constructors=====================================
/**
* 構造函數
* @param 發布方式 0代表嵌入式發布、1代表中心服務器發布模式、2代表rest發布方式、3代表保留未用 (0~3)
* @param workerId 工作ID (0~255)
*/
public IdGenerate(long getMethod, long workerId) {
if (getMethod > maxGetMethod || getMethod < 0) {
throw new IllegalArgumentException(String.format("getMethod can't be greater than %d or less than 0", maxGetMethod));
}
if (workerId > maxWorkerId || workerId < 0) {
throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId));
}
this.getMethod = getMethod;
this.workerId = workerId;
}
public long[] nextId(int nums) {
long[] ids = new long[nums];
for (int i = 0; i < nums; i++) {
ids[i] = nextId();
}
return ids;
}
// ==============================Methods==========================================
/**
* 獲得下一個ID (該方法是線程安全的)
* @return SnowflakeId
*/
public 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) {
try {
lock.lock();
sequence = (sequence + 1) & sequenceMask;
//毫秒內序列溢出
if (sequence == 0) {
//阻塞到下一個毫秒,獲得新的時間戳
timestamp = tilNextMillis(lastTimestamp);
}
}finally {
lock.unlock();
}
}
//時間戳改變,毫秒內序列重置
else {
sequence = 0L;
}
//上次生成ID的時間截
lastTimestamp = timestamp;
//移位並通過或運算拼到一起組成64位的ID
return (getMethod << getMethodShift) // 生成方式占用2位,左移61位
| ((timestamp - twepoch) << timestampShift) // 時間差占用41位,最多69年,左移20位
| (sequence << sequenceShift) // 毫秒內序列,取值范圍0-4095
| workerId; // 工作機器,取值范圍0-255
}
public String nextString() {
return Long.toString(nextId());
}
public String[] nextString(int nums) {
String[] ids = new String[nums];
for (int i = 0; i < nums; i++) {
ids[i] = nextString();
}
return ids;
}
public String nextCode(String prefix) {
StringBuilder sb = new StringBuilder(prefix);
long id = nextId();
sb.append(id);
return sb.toString();
}
/**
* 此方法可以在前綴上增加業務標志
* @param prefix
* @param nums
* @return
*/
public String[] nextCode(String prefix, int nums) {
String[] ids = new String[nums];
for (int i = 0; i < nums; i++) {
ids[i] = nextCode(prefix);
}
return ids;
}
public String nextHexString() {
return Long.toHexString(nextId());
}
/**
* 阻塞到下一個毫秒,直到獲得新的時間戳
* @param lastTimestamp 上次生成ID的時間截
* @return 當前時間戳
*/
protected long tilNextMillis(long lastTimestamp) {
long timestamp = timeGen();
while (timestamp <= lastTimestamp) {
timestamp = timeGen();
}
return timestamp;
}
/**
* 返回以毫秒為單位的當前時間
* @return 當前時間(毫秒)
*/
protected long timeGen() {
return System.currentTimeMillis();
}
//==============================Test=============================================
/**
* 測試
*
*
*/
public static void main(String[] args) {
IdGenerate idGenerate = new IdGenerate(0, 0);
int count = 100000;//線程數=count*count
final long[][] times = new long[count][100];
Thread[] threads = new Thread[count];
for (int i = 0; i < threads.length; i++) {
final int ip = i;
threads[i] = new Thread() {
@Override
public void run() {
for (int j = 0; j <100; j++) {
long t1 = System.nanoTime();//該函數是返回納秒的。1毫秒=1納秒*1000000
idGenerate.nextId();//測試
long t = System.nanoTime() - t1;
times[ip][j] = t;//求平均
}
}
};
}
long lastMilis = System.currentTimeMillis();
//逐個啟動線程
for (int i = 0; i < threads.length; i++) {
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/**
* 1、QPS:系統每秒處理的請求數(query per second)
2、RT:系統的響應時間,一個請求的響應時間,也可以是一段時間的平均值
3、最佳線程數量:剛好消耗完服務器瓶頸資源的臨界線程數
對於單線程:QPS=1000/RT
對於多線程:QPS=1000*線程數量/RT
*/
long time = System.currentTimeMillis() - lastMilis;
System.out
.println("QPS: "
+ (1000*count /time));
long sum = 0;
long max = 0;
for (int i = 0; i < times.length; i++) {
for (int j = 0; j < times[i].length; j++) {
sum += times[i][j];
if (times[i][j] > max)
max = times[i][j];
}
}
System.out.println("Sum(ms)"+time);
System.out.println("AVG(ms): " + sum / 1000000 / (count*100));
System.out.println("MAX(ms): " + max / 1000000);
}
}
測試結果
環境:CPU 雙核I7—6400HQ 系統win10
單線程下每秒產生近40萬個全局ID
模擬單個服務器並發場景:
1000線程並發下每個線程產生100個ID,共生產10萬個ID
- QPS: 2610
- Sum(ms)383
- AVG(ms): 0
- MAX(ms): 9
10000線程並發下每個線程產生100個ID,共生產100萬個ID
- QPS: 2701
- Sum(ms)3701
- AVG(ms): 0
- MAX(ms): 9
50000線程並發下每個線程產生100個ID,共生產500萬個ID
- QPS: 2720
- Sum(ms)18382
- AVG(ms): 0
- MAX(ms): 11