1. 程式人生 > 其它 >seata改進型雪花演算法分散式ID-java實現

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

參考連結

  1. java演算法(4)---靜態內部類實現雪花演算法
  2. Seata基於改良版雪花演算法的分散式UUID生成器分析
  3. 關於新版雪花演算法的答疑
  4. Leaf 美團點評分散式ID生成系統