seata改進型雪花演算法分散式ID-java實現
1,簡介
在複雜分散式系統中,往往需要對大量的資料和訊息進行唯一標識。通俗的講就是,多臺機器支撐一個服務,但是他們生成的id是不重複的,且最好單調遞增(降低mysql B+聚簇索引的頁分類IO頻次)。
當前現有的實現方式有:
實現方式 | 描述 | 優缺點 |
---|---|---|
mysql自增id | 直接使用mysql自帶的自增id功能 |
優點: ①實現簡單 缺點: ①併發效能差 ②依賴於mysql資料庫 |
redis自增鍵值 | 直接使用redis自帶的incr key功能實現 | 優點: ①實現簡單 缺點: ①併發效能差 ②依賴於redis快取 |
uuid | 直接使用程式碼生成的uuid | 優點: ①實現簡單 ②併發效能高 缺點: ①id生成不是單調遞增 |
雪花演算法及其衍生改進型 | 使用標記位+時間戳+節點id+序列號的方式組成 | 優點: ①實現簡單 缺點: ①時鐘回溯問題 ②標準版每個時刻只有4096個併發量 |
Seata改進型雪花演算法 | 使用標記位+節點id+時間戳+序列號的方式組成 | 優點: ①實現簡單 ②併發效能可達409.6W/s ③解決部分時鐘回撥問題 缺點: ①不是全域性單調遞增,只是分機器單調遞增 |
美團leaf分散式id生成框架 | 直接呼叫leaf的分散式id生成服務 | 優點: ①併發效能高 ②解決時鐘回溯問題 缺點: ①需要額外依賴其他服務 |
2,優化策略:
- 時間戳與節點ID互換位置
由原版的標記位(1位)+時間戳(41位)+節點ID(10位)+序列號(12位)
更改為 標記位(1位)+節點ID(10位)+時間戳(41位)+序列號(12位)
- id生成只依賴於初始化時快取的時間戳,不再實時追隨最新時間
3,核心解決問題:
1,解決原有雪花演算法一個ms內4096/ms的效能限制。由於標準版雪花演算法是實時追隨系統時間的,所以1臺機器1個ms內最多隻能生成4096個唯一id;但是改進型只是在系統初始化時快取一次時間戳,之後是在這個時間戳+序列號的組合基礎上進行單調遞增,即便序列號4095繼續向上遞增,也只會超前消費時間戳裡面的位數,不會出現違反唯一性的問題;
2,執行緒安全(使用CAS原子類保證每一個節點ID內安全單調遞增);
3,弱依賴於系統時間。只會在系統啟動的時候快取當前時間戳,之後就不依賴時間戳,即便時鐘小幅度回撥也是不受影響;除非人為的大幅度回撥,那麼會有影響;
4,其他問題:
1,理論上會有併發高的時候序列號消耗完,超前消費時間戳導致資料重複問題的可能。但是,前提是生成器的QPS穩定在4096/ms以上,也就是409.6W/s以上,但是我這邊測試了一下在8C16G的機器上的QPS效能只有26.7W/s,所以說現在的瓶頸已經不是分散式id生成器,這個超前消費的問題現在不用擔心
2,id不隨機問題,這個是美團那邊的leaf分散式id生成器的一個需求,他們怕被競爭對手竊取資料,直接通過一天id的起始值和終止值分析出業務量是多少!這個問題當前確實存在。
3,不是全域性單調遞增,只是分機器單調遞增。這個可以檢視博文【關於新版雪花演算法的答疑】給出的解析,分段單調遞增也是可以減少插入資料的也分裂問題,只不過是分段的段尾進行遞增!
5,java程式碼實現
package site.activeclub.acCore.utils;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicLong;
/**
* seata中優化的分散式雪花演算法生成分段自增id
*/
@Slf4j
@Component
public class SnowflakeIdUtil implements InitializingBean{
/**
* 機器碼移位53
*/
private final long MACHINE_BIT = 53;
/**
* 時間戳移位12
*/
private final long TIMESTAMP_BIT = 12;
/**
*
* business meaning: machine ID (0 ~ 1023)【每個機器碼下對應的id是分段單調遞增】
* actual layout in memory:
* highest 1 bit: 0
* next 10 bit: workerId【機器碼】
* middle 41 bit: timestamp【時間戳】
* lowest 12 bit: sequence【這個時間戳下的自增id,嚴格單調遞增】
*/
private AtomicLong idSequence;
@Value("${snowflake.worker.id:1}")
private long workerId;
/**
* 將機器碼移位到高53位
*/
@Override
public void afterPropertiesSet() {
// 機器碼左移至高位
workerId <<= MACHINE_BIT;
// 跟先前儲存好的高11位進行一個或的位運算
long startId = workerId | (System.currentTimeMillis()<<TIMESTAMP_BIT);
idSequence = new AtomicLong(startId);
}
public long nextId() {
return idSequence.incrementAndGet();
}
public static void main(String[] args) throws InterruptedException {
SnowflakeIdUtil snowflakeIdUtil = new SnowflakeIdUtil();
// 機器碼左移至高位
snowflakeIdUtil.workerId <<= snowflakeIdUtil.MACHINE_BIT;
// 跟先前儲存好的高11位進行一個或的位運算
long startId = snowflakeIdUtil.workerId | (System.currentTimeMillis()<<snowflakeIdUtil.TIMESTAMP_BIT);
snowflakeIdUtil.idSequence = new AtomicLong(startId);
//計時開始時間
long start = System.currentTimeMillis();
//讓100個執行緒同時進行
final CountDownLatch latch = new CountDownLatch(100);
//判斷生成的20萬條記錄是否有重複記錄
final Map<Long, Integer> map = new ConcurrentHashMap();
for (int i = 0; i < 100; i++) {
//建立100個執行緒
new Thread(() -> {
for (int s = 0; s < 20000; s++) {
long snowID =snowflakeIdUtil.nextId();
log.info("生成雪花ID={}",snowID);
Integer put = map.put(snowID, 1);
if (put != null) {
throw new RuntimeException("主鍵重複");
}
}
latch.countDown();
}).start();
}
//讓上面100個執行緒執行結束後,在走下面輸出資訊
latch.await();
log.info("生成20萬條雪花ID總用時={}", System.currentTimeMillis() - start);
}
}
輸出結果:
nowflakeIdUtil - 生成雪花ID=6753605615453437
00:11:48.165 [Thread-30] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615452978
...
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463735
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463736
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463737
00:11:48.201 [Thread-81] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463729
00:11:48.201 [Thread-81] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463739
00:11:48.201 [Thread-74] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463733
00:11:48.201 [Thread-74] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463740
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463738
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463742
00:11:48.201 [Thread-74] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463741
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463743
00:11:48.201 [Thread-63] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成雪花ID=6753605615463744
00:11:48.201 [main] INFO site.activeclub.acCore.utils.SnowflakeIdUtil - 生成20萬條雪花ID總用時=748