原生實現
本文承接sharding-jdbc源碼之分佈式ID,在這篇文章中詳細介紹了sharding-jdbc的分佈式ID是如何實現的;很遺憾的是sharding-jdbc只是基於snowflake算法實現瞭如何生成分佈式ID,並沒有解決snowflake算法的缺點:
- 時鐘回撥問題;
- 趨勢遞增,而不是絕對遞增;
- 不能在一臺服務器上部署多個分佈式ID服務;
第2點算不上缺點,畢竟如果絕對遞增的話,需要犧牲不少的性能;第3點也算不上缺點,即使一臺足夠大內存的服務器,在部署一個分佈式ID服務後,還有很多可用的內存,可以用來部署其他的服務,不一定非得在一臺服務器上部署一個服務的多個實例;可以參考elasticsearch的主分片和副本的劃分規則:某個主分片的副本是不允許和主分片在同一臺節點上--因爲這樣意思不大,如果這個分片只擁有一個副本,且這個節點宕機後,服務狀態就是"RED";
所以這篇文章的主要目的是解決第1個缺點:時鐘回撥問題
;先來看一下sharding-jdbc的DefaultKeyGenerator.java中generateKey()
方法的源碼:
@Override
public synchronized Number generateKey() {
long currentMillis = timeService.getCurrentMillis();
Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
if (lastTime == currentMillis) {
if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
currentMillis = waitUntilNextTime(currentMillis);
}
} else {
sequence = 0;
}
lastTime = currentMillis;
if (log.isDebugEnabled()) {
log.debug("{}-{}-{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime)), workerId, sequence);
}
return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
說明:從這段代碼可知,sharding-jdbc並沒有嘗試去解決snowflake算法時鐘回撥的問題,只是簡單判斷如果
lastTime <= currentMillis
不滿足就拋出異常:Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
改進思路
snowflake算法一個很大的優點就是不需要依賴任何第三方組件,筆者想繼續保留這個優點:畢竟多一個依賴的組件,多一個風險,並增加了系統的複雜性。
snowflake算法給workerId預留了10位,即workId的取值範圍爲[0, 1023],事實上實際生產環境不大可能需要部署1024個分佈式ID服務,所以:將workerId取值範圍縮小爲[0, 511],[512, 1023]這個範圍的workerId當做備用workerId。workId爲0的備用workerId是512,workId爲1的備用workerId是513,以此類推……
說明:如果你的業務真的需要512個以上分佈式ID服務才能滿足需求,那麼不需要繼續往下看了,這個方案不適合你^^;
改進實現
對generateKey()
方法的改進如下:
// 修改處: workerId原則上上限爲1024, 但是爲了每臺sequence服務預留一個workerId, 所以實際上workerId上限爲512
private static final long WORKER_ID_MAX_VALUE = 1L << WORKER_ID_BITS >> 1;
/**
* 保留workerId和lastTime, 以及備用workerId和其對應的lastTime
*/
private static Map<Long, Long> workerIdLastTimeMap = new ConcurrentHashMap<>();
/**
* Generate key. 考慮時鐘回撥, 與sharding-jdbc源碼的區別就在這裏</br>
* 缺陷: 如果連續兩次時鐘回撥, 可能還是會有問題, 但是這種概率極低極低
* @return key type is @{@link Long}.
* @Author 阿飛
*/
@Override
public synchronized Number generateKey() {
long currentMillis = System.currentTimeMillis();
// 當發生時鐘回撥時
if (lastTime > currentMillis){
// 如果時鐘回撥在可接受範圍內, 等待即可
if (lastTime - currentMillis < MAX_BACKWARD_MS){
try {
Thread.sleep(lastTime - currentMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
// 如果時鐘回撥太多, 那麼換備用workerId嘗試
// 當前workerId和備用workerId的值的差值爲512
long interval = 512L;
// 發生時鐘回撥時, 計算備用workerId[如果當前workerId小於512,
// 那麼備用workerId=workerId+512; 否則備用workerId=workerId-512, 兩個workerId輪換用]
if (MyKeyGenerator.workerId >= interval) {
MyKeyGenerator.workerId = MyKeyGenerator.workerId - interval;
} else {
MyKeyGenerator.workerId = MyKeyGenerator.workerId + interval;
}
// 取得備用workerId的lastTime
Long tempTime = workerIdLastTimeMap.get(MyKeyGenerator.workerId);
lastTime = tempTime==null?0L:tempTime;
// 如果在備用workerId也處於過去的時鐘, 那麼拋出異常
// [這裏也可以增加時鐘回撥是否超過MAX_BACKWARD_MS的判斷]
Preconditions.checkState(lastTime <= currentMillis, "Clock is moving backwards, last time is %d milliseconds, current time is %d milliseconds", lastTime, currentMillis);
// 備用workerId上也處於時鐘回撥範圍內的邏輯還可以優化: 比如摘掉當前節點. 運維通過監控發現問題並修復時鐘回撥
}
}
// 如果和最後一次請求處於同一毫秒, 那麼sequence+1
if (lastTime == currentMillis) {
if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
currentMillis = waitUntilNextTime(currentMillis);
}
} else {
// 如果是一個更近的時間戳, 那麼sequence歸零
sequence = 0;
}
lastTime = currentMillis;
// 更新map中保存的workerId對應的lastTime
workerIdLastTimeMap.put(MyKeyGenerator.workerId, lastTime);
if (log.isDebugEnabled()) {
log.debug("{}-{}-{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime)), workerId, sequence);
}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime))
+" -- "+workerId+" -- "+sequence+" -- "+workerIdLastTimeMap);
return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
改進驗證
Powered By 阿飛Javaer.png
第一個紅色箭頭的地方通過修改本地時間,通過啓動了備用workerId從而避免了時鐘回撥60s(window系統直接修改系統時間模擬)引起的問題;第二個紅色箭頭在60s內再次模擬時鐘回撥,就會有問題,因爲無論是workerId還是備用workerId都會有衝突;如果第二紅色箭頭是60s後模擬時鐘回撥,依然可以避免問題,原因嘛,你懂得;
再次優化
每個workerId可以配置任意個備用workerId,由使用者去平衡sequence服務的性能及高可用,終極版代碼如下:
public final class MyKeyGenerator implements KeyGenerator {
private static final long EPOCH;
private static final long SEQUENCE_BITS = 12L;
private static final long WORKER_ID_BITS = 10L;
private static final long SEQUENCE_MASK = (1 << SEQUENCE_BITS) - 1;
private static final long WORKER_ID_LEFT_SHIFT_BITS = SEQUENCE_BITS;
private static final long TIMESTAMP_LEFT_SHIFT_BITS = WORKER_ID_LEFT_SHIFT_BITS + WORKER_ID_BITS;
/**
* 每臺workerId服務器有3個備份workerId, 備份workerId數量越多, 可靠性越高, 但是可部署的sequence ID服務越少
*/
private static final long BACKUP_COUNT = 3;
/**
* 實際的最大workerId的值<br/>
* workerId原則上上限爲1024, 但是需要爲每臺sequence服務預留BACKUP_AMOUNT個workerId,
*/
private static final long WORKER_ID_MAX_VALUE = (1L << WORKER_ID_BITS) / (BACKUP_COUNT + 1);
/**
* 目前用戶生成ID的workerId
*/
private static long workerId;
static {
Calendar calendar = Calendar.getInstance();
calendar.set(2018, Calendar.NOVEMBER, 1);
calendar.set(Calendar.HOUR_OF_DAY, 0);
calendar.set(Calendar.MINUTE, 0);
calendar.set(Calendar.SECOND, 0);
calendar.set(Calendar.MILLISECOND, 0);
// EPOCH是服務器第一次上線時間點, 設置後不允許修改
EPOCH = calendar.getTimeInMillis();
}
private long sequence;
private long lastTime;
/**
* 保留workerId和lastTime, 以及備用workerId和其對應的lastTime
*/
private static Map<Long, Long> workerIdLastTimeMap = new ConcurrentHashMap<>();
static {
// 初始化workerId和其所有備份workerId與lastTime
// 假設workerId爲0且BACKUP_AMOUNT爲4, 那麼map的值爲: {0:0L, 256:0L, 512:0L, 768:0L}
// 假設workerId爲2且BACKUP_AMOUNT爲4, 那麼map的值爲: {2:0L, 258:0L, 514:0L, 770:0L}
for (int i = 0; i<= BACKUP_COUNT; i++){
workerIdLastTimeMap.put(workerId + (i * WORKER_ID_MAX_VALUE), 0L);
}
System.out.println("workerIdLastTimeMap:" + workerIdLastTimeMap);
}
/**
* 最大容忍時間, 單位毫秒, 即如果時鐘只是回撥了該變量指定的時間, 那麼等待相應的時間即可;
* 考慮到sequence服務的高性能, 這個值不易過大
*/
private static final long MAX_BACKWARD_MS = 3;
/**
* Set work process id.
* @param workerId work process id
*/
public static void setWorkerId(final long workerId) {
Preconditions.checkArgument(workerId >= 0L && workerId < WORKER_ID_MAX_VALUE);
MyKeyGenerator.workerId = workerId;
}
/**
* Generate key. 考慮時鐘回撥, 與sharding-jdbc源碼的區別就在這裏</br>
* 缺陷: 如果連續兩次時鐘回撥, 可能還是會有問題, 但是這種概率極低極低
* @return key type is @{@link Long}.
* @Author 阿飛
*/
@Override
public synchronized Number generateKey() {
long currentMillis = System.currentTimeMillis();
// 當發生時鐘回撥時
if (lastTime > currentMillis){
// 如果時鐘回撥在可接受範圍內, 等待即可
if (lastTime - currentMillis < MAX_BACKWARD_MS){
try {
Thread.sleep(lastTime - currentMillis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
tryGenerateKeyOnBackup(currentMillis);
}
}
// 如果和最後一次請求處於同一毫秒, 那麼sequence+1
if (lastTime == currentMillis) {
if (0L == (sequence = ++sequence & SEQUENCE_MASK)) {
currentMillis = waitUntilNextTime(currentMillis);
}
} else {
// 如果是一個更近的時間戳, 那麼sequence歸零
sequence = 0;
}
lastTime = currentMillis;
// 更新map中保存的workerId對應的lastTime
workerIdLastTimeMap.put(MyKeyGenerator.workerId, lastTime);
if (log.isDebugEnabled()) {
log.debug("{}-{}-{}", new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime)), workerId, sequence);
}
System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS").format(new Date(lastTime))
+" -- "+workerId+" -- "+sequence+" -- "+workerIdLastTimeMap);
return ((currentMillis - EPOCH) << TIMESTAMP_LEFT_SHIFT_BITS) | (workerId << WORKER_ID_LEFT_SHIFT_BITS) | sequence;
}
/**
* 嘗試在workerId的備份workerId上生成
* @param currentMillis 當前時間
*/
private long tryGenerateKeyOnBackup(long currentMillis){
System.out.println("try GenerateKey OnBackup, map:" + workerIdLastTimeMap);
// 遍歷所有workerId(包括備用workerId, 查看哪些workerId可用)
for (Map.Entry<Long, Long> entry:workerIdLastTimeMap.entrySet()){
MyKeyGenerator.workerId = entry.getKey();
// 取得備用workerId的lastTime
Long tempLastTime = entry.getValue();
lastTime = tempLastTime==null?0L:tempLastTime;
// 如果找到了合適的workerId
if (lastTime<=currentMillis){
return lastTime;
}
}
// 如果所有workerId以及備用workerId都處於時鐘回撥, 那麼拋出異常
throw new IllegalStateException("Clock is moving backwards, current time is "
+currentMillis+" milliseconds, workerId map = " + workerIdLastTimeMap);
}
private long waitUntilNextTime(final long lastTime) {
long time = System.currentTimeMillis();
while (time <= lastTime) {
time = System.currentTimeMillis();
}
return time;
}
}
核心優化代碼在方法
tryGenerateKeyOnBackup()
中,BACKUP_COUNT
即備份workerId數越多,sequence服務避免時鐘回撥影響的能力越強,但是可部署的sequence服務越少,設置BACKUP_COUNT
爲3,最多可以部署1024/(3+1)即256個sequence服務,完全夠用,抗時鐘回撥影響的能力也得到非常大的保障。
改進總結
這種改進方案最大優點就是沒有引入任何第三方中間件(例如redis,zookeeper等),但是避免時鐘回撥能力得到極大的提高,而且時鐘回撥本來就是極小概率。阿飛Javaer認爲這種方案能夠達到絕大部分sequence服務的需求了;
作者:阿飛的博客
鏈接:https://www.jianshu.com/p/98c202f64652
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。