1. 程式人生 > >基於Spring Boot的可直接執行的分散式ID生成器的實現以及SnowFlake演算法詳解

基於Spring Boot的可直接執行的分散式ID生成器的實現以及SnowFlake演算法詳解

背景

最近對snowflake比較感興趣,就看了一些分散式唯一ID生成器(發號器)的開源專案的原始碼,例如百度的uid-generator,美團的leaf。大致看了一遍後感覺uid-generator程式碼寫的要更好一些,十分的精煉,短小精悍。

正好手頭有個任務要搞個發號器,百度的這個原始碼是不能直接執行起來提供服務的,為了練練手,就把百度的uid-generator遷移到spring boot上重寫了一遍。

程式碼基本一模一樣,就做了一些工程化的東西,讓uid-generator能以服務的形式跑起來,通過http介面對外提供服務。

可執行的程式碼地址:點這裡

SnowFlake資料結構

 

這裡借用一下uid-generator的圖:

 

 

 

這一個結構就是一個snowflake演算法裡的id,共計64位,就是一個long。

  • sign是一個恆為0的值,是為了保證算出的id恆為正數。

 

  • delta seconds (28 bits)

        當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,最多可支援約8.7年。時間基點是自己配置的。28位即最大表示2^28的數值的秒數,換算一下就是8.7年左右。

 

  • worker id (22 bits)

        機器id,最多可支援約420w次機器啟動。內建實現為在啟動時由資料庫分配。420w = 2^22

 

  • sequence (13 bits)

         每秒下的併發序列,13 bits可支援每秒8192個併發,即2^13個併發

 

這些位數都是可以改變的,對於很多公司來說,28位 delta seconds帶來的8.7年的最大支援時間是可預期的不夠用的,而22bit的worker id和13bit的sequence則是遠遠超出可預期的業務場景的,那麼就可以自由的根據自己的需求,對這三個引數進行調整。

例如,{"workerBits":20,"timeBits":31,"seqBits":12}這樣的配置可以68年,100W次重啟,單機每秒4096個併發的情況,個人感覺還是比較合適的。

 

snowflake的實現有很多種方式,不過思想上都是一樣的。

 

SnowFlake發號實現

在瞭解SnowFlake的資料結構後,就可以來看看具體是如何生成ID的了。

其實這個過程,就是往delta seconds,sequence,worker id三個結構裡填充資料的過程。

整體類圖如下:

 

 

SnowFlakeGenerator就是基於SnowFlake演算法的UidGenerator的實現類,SnowFlake的實現就是在這個類裡;

BitsAllocator就是對SnowFlake 的ID進行位操作的共聚類;

DatabaseWorkerIdAssigner就是一個基於DB自增的worker id 分配器的實現。

 

BitsAllocator

這個類是進行一些位操作的工具類,給每一個id 的delta seconds,sequence,worker id賦值就是通過這個類來實現的。這個類有以下成員變數:

/**
     * Total 64 bits
     */
    public static final int TOTAL_BITS = 1 << 6;

    /**
     * Bits for [sign-> second-> workId-> sequence]
     */
    private int signBits = 1;
    private final int timestampBits;
    private final int workerIdBits;
    private final int sequenceBits;

    /**
     * Max value for workId & sequence
     */
    private final long maxDeltaSeconds;
    private final long maxWorkerId;
    private final long maxSequence;

    /**
     * Shift for timestamp & workerId
     */
    /**
     * timestamp需要位移多少位
     */
    private final int timestampShift;
    /**
     * workerId需要位移多少位
     */
    private final int workerIdShift;

 

其他欄位都好說,看名稱和註釋都能明白。最下面倆shift,可能現在看著有些摸不著頭腦,不過看後面的賦值過程就知道什麼叫“shift”了

構造器:

    /**
     * Constructor with timestampBits, workerIdBits, sequenceBits<br>
     * The highest bit used for sign, so <code>63</code> bits for timestampBits, workerIdBits, sequenceBits
     */
    public BitsAllocator(int timestampBits, int workerIdBits, int sequenceBits) {
        // make sure allocated 64 bits
        int allocateTotalBits = signBits + timestampBits + workerIdBits + sequenceBits;
        Assert.isTrue(allocateTotalBits == TOTAL_BITS, "allocate not enough 64 bits");

        // initialize bits
        this.timestampBits = timestampBits;
        this.workerIdBits = workerIdBits;
        this.sequenceBits = sequenceBits;

        // initialize max value
        //先將-1左移timestampBits位,得到-10000...(timestampBits-1個零)
        //然後取反,得到1111(timestampBits-1)個1
        //等價於2的timestampBits次方-1
        this.maxDeltaSeconds = ~(-1L << timestampBits);
        this.maxWorkerId = ~(-1L << workerIdBits);
        this.maxSequence = ~(-1L << sequenceBits);

        // initialize shift
        this.timestampShift = workerIdBits + sequenceBits;
        this.workerIdShift = sequenceBits;
    }

 

也很簡單,重點就在 “~(-1L << timestampBits) ”這樣一坨操作,可能理解起來會有些困難。這是一連串的位操作,這裡進行一下分解:

  • -1 左移 timestampBits 位,也就是的到了-(2^timestampBits) 。實際的二進位制看起來是10000......10000...(最前面的1是最高位,表示負數;第二個1後面有timestampBits-1個零)
  • 對-(2^timestampBits)進行取反操作,的到了2的timestampBits次方-1。實際的二進位制看起來就是1111(timestampBits-1個1)

這一通操作其實也就相當於2的timestampBits次方-1,也就是timestampBits位二進位制最大能表示的數字,不過是用位運算來做的。如果不懂二進位制的位移和取反,可以百度“位操作”補充一下基礎,這裡就不展開了。

 

分配操作:

    /**
     * Allocate bits for UID according to delta seconds & workerId & sequence<br>
     * <b>Note that: </b>The highest bit will always be 0 for sign
     *
     * 這裡就是把不同的欄位放到相應的位上
     * id的總體結構是:
     * sign (fixed 1bit) -> deltaSecond -> workerId -> sequence(within the same second)
     * deltaSecond 左移(workerIdBits + sequenceBits)位,workerId左移sequenceBits位,此時就完成了位元組的分配
     * @param deltaSeconds
     * @param workerId
     * @param sequence
     * @return
     */
    public long allocate(long deltaSeconds, long workerId, long sequence) {
        return (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
    }

 

這裡就是對delta seconds,sequence,worker id三個結構進行賦值的地方了,核心程式碼之一。可以再看一下最上面的圖,sequence是在最右側(最低位),所以sequence不用做位移,直接就是在對的位置;

而workerId,需要左移workerIdShift才能到正確的位置。workerIdShift看上面的構造器,就是sequenceBits,就是sequence的位數;

deltaSeconds 左移timestampShift位,也就是workerIdBits + sequenceBits;

然後對這三個位移後的值進行“或”操作,就把正確的值賦到正確的位數上了。

 

DatabaseWorkerIdAssigner

SnowFlake中,deltaSeconds依賴時間戳,可以通過系統獲取;sequence可以通過自增來控制;這倆欄位都是專案可以自給自足的,而WorkerId則必須還有一個策略來提供。

這個策略要保證每次服務啟動的時候拿到的WorkerId都能不重複,不然就有可能叢集不同的機器拿到不同的workerid,會發重複的號了;

而服務啟動又是個相對低頻的行為,也不影響發號效能,所以可以用DB自增ID來實現。

DatabaseWorkerIdAssigner就是依賴DB自增ID實現的workerId分配器。

程式碼就不貼了,就是個簡單的save然後取到DB的自增ID。

 

SnowFlakeGenerator

這裡就是控制發號邏輯的地方了。

先看看成員變數和初始化部分:

@Value("${snowflake.timeBits}")
    protected int timeBits = 28;

    @Value("${snowflake.workerBits}")
    protected int workerBits = 22;

    @Value("${snowflake.seqBits}")
    protected int seqBits = 13;

    @Value("${snowflake.epochStr}")
    /** Customer epoch, unit as second. For example 2016-05-20 (ms: 1463673600000)*/
    protected String epochStr = "2016-05-20";
    protected long epochSeconds = TimeUnit.MILLISECONDS.toSeconds(1463673600000L);

    @Autowired
    @Qualifier(value = "dbWorkerIdAssigner")
    protected WorkerIdAssigner workerIdAssigner;

    /** Stable fields after spring bean initializing */
    protected BitsAllocator bitsAllocator;


    protected long workerId;


    /** Volatile fields caused by nextId() */
    protected long sequence = 0L;
    protected long lastSecond = -1L;


    @PostConstruct
    public void afterPropertiesSet() throws Exception {
        bitsAllocator = new BitsAllocator(timeBits,workerBits,seqBits);
        // initialize worker id
        workerId = workerIdAssigner.assignWorkerId();

        if(workerId > bitsAllocator.getMaxWorkerId()){
            throw new RuntimeException("Worker id " + workerId + " exceeds the max " + bitsAllocator.getMaxWorkerId());
        }

        if (StringUtils.isNotBlank(epochStr)) {
            this.epochSeconds = TimeUnit.MILLISECONDS.toSeconds(DateUtils.parseByDayPattern(epochStr).getTime());
        }

        log.info("Initialized bits(1, {}, {}, {}) for workerID:{}", timeBits, workerBits, seqBits, workerId);
    }

@Value注入的都是配置檔案裡讀取的值。

afterPropertiesSet裡,將配置檔案讀取到的值傳遞給BitsAllocator,夠造出一個對應的BitsAllocator;

然後生成一個workerId(插入一條DB記錄),初始化過程就完成了。

 

再看核心發號控制邏輯:

/**
     * Get UID
     *
     * @return UID
     * @throws UidGenerateException in the case: Clock moved backwards; Exceeds the max timestamp
     */
    protected synchronized long nextId() {
        long currentSecond = getCurrentSecond();

        // Clock moved backwards, refuse to generate uid
        //todo 時鐘回撥問題待解決
        if (currentSecond < lastSecond) {
            long refusedSeconds = lastSecond - currentSecond;
            throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
        }

        // At the same second, increase sequence
        //同一秒內的,seq加一
        if (currentSecond == lastSecond) {
            //seq 加一,如果大於MaxSequence,就變成0
            //如果大於MaxSequence 就是seq能取到的最大值,二進位制(seqBits -1)位全是1
            sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
            // Exceed the max sequence, we wait the next second to generate uid
            //號發完了,等到下一秒
            if (sequence == 0) {
                currentSecond = getNextSecond(lastSecond);
            }

            // At the different second, sequence restart from zero
        } else {
            //新的一秒,重新開始發號
            sequence = 0L;
        }
        lastSecond = currentSecond;
        // Allocate bits for UID
        return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
    }

 

注意這是個synchronized方法,這是關鍵。

getCurrentSecond就是獲取當前以秒為單位的時間戳;

sequence計算邏輯

如果currentSecond和lastSecond一樣,那說明本次發號請求不是本秒的第一次,只要將sequence直接+1即可;如果+1後大於了MaxSequence(這裡會用& bitsAllocator.getMaxSequence()設定為0),那說明本秒的sequence已經用完了,此時請求已經超出了本秒系統的最大吞吐量,這裡需要呼叫getNextSecond(詳見github),來等待到下一秒;

如果currentSecond和lastSecond不一樣,說名本次請求是全新的一秒,這時候sequence設定為0即可。

 

deltaSecond計算邏輯

就是currentSecond - epochSeconds,當前時間減去初始時間的秒數。

 

此時,workerId,deltaSecond,sequence都已經確定了具體的值,然後呼叫bitsAllocator.allocate方法,就可以生成一個全新的ID了,至此發號完成。