百度 - UidGenerator源碼解析

百度 - UidGenerator源碼解析

簡介

UidGenerator是Java實現的,基於Snowflake算法的唯一ID生成器。UidGenerator以組件形式工作在應用項目中,支持自定義workerId位數和初始化策略,從而適用於Docker等虛擬化環境下實例自動重啓、漂移等場景。 在實現上,UidGenerator通過採用RingBuffer來緩存已生成的UID,並行化UID的生產和消費,同時對CacheLine補齊,避免了由RingBuffer帶來的硬件級「僞共享」問題。

snowflake算法

uid-generator是基於Twitter開源的snowflake算法實現的。

snowflake將long的64位分爲了3部分,時間戳、工作機器id和序列號,位數分配如下。

img

其中,時間戳部分的時間單位一般爲毫秒。也就是說1臺工作機器1毫秒可產生4096個id(2的12次方)。

源碼分析

本文基於commit id ba696f535ba6b000b96dd73a7b697e4a00c88085所寫,爲編寫本文時(2017-08-02 21:59:47)的最新的Master分支,閱讀時須注意未來的版本迭代有可能造成功能上的差異。

目錄結構

 com
 └── baidu
     └── fsg
         └── uid
             ├── BitsAllocator.java			- Bit分配器(C)
             ├── UidGenerator.java			- UID生成的接口(I)
             ├── buffer
             │   ├── BufferPaddingExecutor.java		- 填充RingBuffer的執行器(C)
             │   ├── BufferedUidProvider.java		- RingBuffer中UID的提供者(C)
             │   ├── RejectedPutBufferHandler.java	- 拒絕Put到RingBuffer的處理器(C)
             │   ├── RejectedTakeBufferHandler.java	- 拒絕從RingBuffer中Take的處理器(C)
             │   └── RingBuffer.java			- 內含兩個環形數組(C)
             ├── exception
             │   └── UidGenerateException.java		- 運行時異常
             ├── impl
             │   ├── CachedUidGenerator.java		- RingBuffer存儲的UID生成器(C)
             │   └── DefaultUidGenerator.java		- 無RingBuffer的默認UID生成器(C)
             ├── utils
             │   ├── DateUtils.java
             │   ├── DockerUtils.java
             │   ├── EnumUtils.java
             │   ├── NamingThreadFactory.java
             │   ├── NetUtils.java
             │   ├── PaddedAtomicLong.java
             │   └── ValuedEnum.java
             └── worker
                 ├── DisposableWorkerIdAssigner.java	- 用完即棄的WorkerId分配器(C)
                 ├── WorkerIdAssigner.java		- WorkerId分配器(I)
                 ├── WorkerNodeType.java		- 工作節點類型(E)
                 ├── dao
                 │   └── WorkerNodeDAO.java		- MyBatis Mapper
                 └── entity
                     └── WorkerNodeEntity.java		- MyBatis Entity

組件功能簡述

UidGenerator在應用中是以Spring組件的形式提供服務,DefaultUidGenerator提供了最簡單的Snowflake式的生成模式,但是並沒有使用任何緩存來預存UID,在需要生成ID的時候即時進行計算。而CachedUidGenerator是一個使用RingBuffer預先緩存UID的生成器,在初始化時就會填充整個RingBuffer,並在take()時檢測到少於指定的填充閾值之後就會異步地再次填充RingBuffer(默認值爲50%),另外可以啓動一個定時器週期性檢測閾值並及時進行填充。

本文將着重介紹CachedUidGenerator及其背後的組件是如何運作的,在此之前我們先了解某些核心類是如何運轉。

uid簡述

與原始的snowflake算法不同,uid-generator支持自定義時間戳、工作機器id和序列號等各部分的位數,以應用於不同場景。默認分配方式如下。

  • sign(1bit)
    固定1bit符號標識,即生成的UID爲正數。
  • delta seconds (28 bits)
    當前時間,相對於時間基點"2016-05-20"的增量值,單位:秒,最多可支持約8.7年(注意:1. 這裏的單位是秒,而不是毫秒! 2.注意這裏的用詞,是“最多”可支持8.7年,爲什麼是“最多”,後面會講)
  • worker id (22 bits)
    機器id,最多可支持約420w次機器啓動。內置實現爲在啓動時由數據庫分配,默認分配策略爲用後即棄,後續可提供複用策略。
  • sequence (13 bits)
    每秒下的併發序列,13 bits可支持每秒8192個併發。(注意下這個地方,默認支持qps最大爲8192個)

DefaultUidGenerator

DefaultUidGenerator的產生id的方法與基本上就是常見的snowflake算法實現,僅有一些不同,如以秒爲爲單位而不是毫秒。

DefaultUidGenerator的產生id的方法如下:

protected synchronized long nextId() {
        long currentSecond = getCurrentSecond();

        // Clock moved backwards, refuse to generate uid
        if (currentSecond < lastSecond) {
            long refusedSeconds = lastSecond - currentSecond;
            throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
        }

        // At the same second, increase sequence
        if (currentSecond == lastSecond) {
            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);
    }

CachedUidGenerator

CachedUidGenerator支持緩存生成的id。

基本實現原理

關於CachedUidGenerator,文檔上是這樣介紹的。

在實現上, UidGenerator通過借用未來時間來解決sequence天然存在的併發限制; 採用RingBuffer來緩存已生成的UID, 並行化UID的生產和消費, 同時對CacheLine補齊,避免了由RingBuffer帶來的硬件級「僞共享」問題. 最終單機QPS可達600萬。

【採用RingBuffer來緩存已生成的UID, 並行化UID的生產和消費】

因爲delta seconds部分是以秒爲單位的,所以1個worker 1秒內最多生成的id書爲8192個(2的13次方)。

從上可知,支持的最大qps爲8192,所以通過緩存id來提高吞吐量。

爲什麼叫藉助未來時間?

因爲每秒最多生成8192個id,當1秒獲取id數多於8192時,RingBuffer中的id很快消耗完畢,在填充RingBuffer時,生成的id的delta seconds 部分只能使用未來的時間。

(因爲使用了未來的時間來生成id,所以上面說的是,【最多】可支持約8.7年)

BitsAllocator - Bit分配器

整個UID由64bit組成,以下圖爲例,1bit是符號位,其餘63位由deltaSecondsworkerIdsequence組成,注意sequence被放在最後,可方便直接進行求和或自增操作

該類主要接收上述3個用於組成UID的元素,並計算出各個元素的最大值和對應的位偏移。其申請UID時的方法如下,由這3個元素進行或操作進行拼接。

 /** * Allocate bits for UID according to delta seconds & workerId & sequence<br> * <b>Note that: </b>The highest bit will always be 0 for sign * * @param deltaSeconds * @param workerId * @param sequence * @return */
    public long allocate(long deltaSeconds, long workerId, long sequence) {
        return (deltaSeconds << timestampShift) | (workerId << workerIdShift) | sequence;
    }

DisposableWorkerIdAssigner - Worker ID分配器

本類用於爲每個工作機器分配一個唯一的ID,目前來說是用完即棄,在初始化Bean的時候會自動向MySQL中插入一條關於該服務的啓動信息,待MySQL返回其自增ID之後,使用該ID作爲工作機器ID並柔和到UID的生成當中。

 @Transactional
    public long assignWorkerId() {
        // build worker node entity
        WorkerNodeEntity workerNodeEntity = buildWorkerNode();

        // add worker node for new (ignore the same IP + PORT)
        workerNodeDAO.addWorkerNode(workerNodeEntity);
        LOGGER.info("Add worker node:" + workerNodeEntity);

        return workerNodeEntity.getId();
    }

buildWorkerNode()爲獲取該啓動服務的信息,兼容Docker服務。

RingBuffer - 用於存儲UID的雙環形數組結構

我們先看RingBuffer的field outline,這樣能大致瞭解到他的工作模式:

/** * Constants */
    private static final int START_POINT = -1;
    private static final long CAN_PUT_FLAG = 0L;
    private static final long CAN_TAKE_FLAG = 1L;
	// 默認擴容閾值
    public static final int DEFAULT_PADDING_PERCENT = 50;

    /** * The size of RingBuffer's slots, each slot hold a UID * <p> * buffer的大小爲2^n */
    private final int bufferSize;
    /** * 因爲bufferSize爲2^n,indexMask爲bufferSize-1,作爲被餘數可快速取模 */
    private final long indexMask;
    /** * 盛裝UID的數組 */
    private final long[] slots;
    /** * 盛裝flag的數組(是否可讀或者可寫) */
    private final PaddedAtomicLong[] flags;

    /** * Tail: last position sequence to produce */
    private final AtomicLong tail = new PaddedAtomicLong(START_POINT);

    /** * Cursor: current position sequence to consume */
    private final AtomicLong cursor = new PaddedAtomicLong(START_POINT);

    /** * Threshold for trigger padding buffer */
    private final int paddingThreshold;

    /** * Reject putbuffer handle policy * <p> * 拒絕方式爲打印日誌 */
    private RejectedPutBufferHandler rejectedPutHandler = this::discardPutBuffer;
    /** * Reject take buffer handle policy * <p> * 拒絕方式爲拋出異常並打印日誌 */
    private RejectedTakeBufferHandler rejectedTakeHandler = this::exceptionRejectedTakeBuffer;

    /** * Executor of padding buffer * <p> * 填充RingBuffer的executor */
    private BufferPaddingExecutor bufferPaddingExecutor;

RingBuffer內兩個環形數組,一個名爲slots的用於存放UID的long類型數組,另一個名爲flags的用於存放讀寫標識的PaddedAtomicLong類型數組。

即使是不同線程間對slots進行**串行寫操作(下文會詳述)**在多核處理器下應該也會使得該數組發生僞共享問題,因爲Java線程在目前來說並不能綁定CPU,所以在修改相同的Cache Line的時候,是有十分可能產生RFO信號的。

那爲什麼一個使用long而另一個使用PaddedAtomicLong呢?

原因是slots數組選用原生類型是爲了高效地讀取,數組在內存中是連續分配的,當你讀取第0個元素的之後,後面的若干個數組元素也會同時被加載。分析代碼即可發現slots實質是屬於多讀少寫的變量,所以使用原生類型的收益更高。而flags則是會頻繁進行寫操作,爲了避免僞共享問題所以手工進行補齊。如果使用的是JDK8,也可以使用註解sun.misc.Contended在類或者字段上聲明,在使用JVM參數-XX:-RestrictContended時會自動進行補齊。

RingBuffer的填充操作

我們需要注意的是put(long)方法是一個同步方法,換句話說就是串行寫,保證了填充slot和移動tail是原子操作。

  /** * Put an UID in the ring & tail moved<br> * We use 'synchronized' to guarantee the UID fill in slot & publish new tail sequence as atomic operations<br> * <p> * <b>Note that: </b> It is recommended to put UID in a serialize way, cause we once batch generate a series UIDs and put * the one by one into the buffer, so it is unnecessary put in multi-threads * * @param uid * @return false means that the buffer is full, apply {@link RejectedPutBufferHandler} */
    public synchronized boolean put(long uid) {
        long currentTail = tail.get();
        long currentCursor = cursor.get();
        // 首次put時,currentTail爲-1,currentCursor爲0,此時distance爲-1
        long distance = currentTail - (currentCursor == START_POINT ? 0 : currentCursor);
        // tail catches the cursor, means that you can't put anything cause of RingBuffer is full
        if (distance == bufferSize - 1) {
            rejectedPutHandler.rejectPutBuffer(this, uid);
            return false;
        }

        // 1. pre-check whether the flag is CAN_PUT_FLAG
        // 首次put時,currentTail爲-1
        int nextTailIndex = calSlotIndex(currentTail + 1);
        if (flags[nextTailIndex].get() != CAN_PUT_FLAG) {
            rejectedPutHandler.rejectPutBuffer(this, uid);
            return false;
        }

        // 2. put UID in the next slot
        slots[nextTailIndex] = uid;
        // 3. update next slot' flag to CAN_TAKE_FLAG
        flags[nextTailIndex].set(CAN_TAKE_FLAG);
        // 4. publish tail with sequence increase by one
        tail.incrementAndGet();

        // The atomicity of operations above, guarantees by 'synchronized'. In another word,
        // the take operation can't consume the UID we just put, until the tail is published(tail.incrementAndGet())
        return true;
    }

RingBuffer的讀取操作

UID的讀取是一個lock free操作,使用CAS成功將tail往後移動之後即視爲線程安全。

/** * Take an UID of the ring at the next cursor, this is a lock free operation by using atomic cursor<p> * <p> * Before getting the UID, we also check whether reach the padding threshold, * the padding buffer operation will be triggered in another thread<br> * If there is no more available UID to be taken, the specified {@link RejectedTakeBufferHandler} will be applied<br> * * @return UID * @throws IllegalStateException if the cursor moved back */
    public long take() {
        // spin get next available cursor
        long currentCursor = cursor.get();
        // cursor初始化爲-1,現在cursor等於tail,所以初始化時nextCursor爲-1
        long nextCursor = cursor.updateAndGet(old -> old == tail.get() ? old : old + 1);

        // check for safety consideration, it never occurs
        // 初始化或者全部UID耗盡時nextCursor == currentCursor
        Assert.isTrue(nextCursor >= currentCursor, "Curosr can't move back");

        // trigger padding in an async-mode if reach the threshold
        long currentTail = tail.get();
        // 會有多個線程去觸發padding事件,但最終只會有一條線程執行padding操作
        if (currentTail - nextCursor < paddingThreshold) {
            LOGGER.info("Reach the padding threshold:{}. tail:{}, cursor:{}, rest:{}", paddingThreshold, currentTail,
                    nextCursor, currentTail - nextCursor);
            bufferPaddingExecutor.asyncPadding();	// (a)
        }

        // cursor catch the tail, means that there is no more available UID to take
        if (nextCursor == currentCursor) {
            rejectedTakeHandler.rejectTakeBuffer(this);
        }

        // 1. check next slot flag is CAN_TAKE_FLAG
        int nextCursorIndex = calSlotIndex(nextCursor);
        // 這個位置必須要是可以TAKE
        Assert.isTrue(flags[nextCursorIndex].get() == CAN_TAKE_FLAG, "Curosr not in can take status");

        // 2. get UID from next slot
        // 取出UID
        long uid = slots[nextCursorIndex];
        // 3. set next slot flag as CAN_PUT_FLAG.
        // 告知flags數組這個位置是可以被重用了
        flags[nextCursorIndex].set(CAN_PUT_FLAG);

        // Note that: Step 2,3 can not swap. If we set flag before get value of slot, the producer may overwrite the
        // slot with a new UID, and this may cause the consumer take the UID twice after walk a round the ring
        return uid;
    }

(a)處可以看到當達到默認填充閾值50%時,即slots被消費大於50%的時候進行異步填充,這個填充由BufferPaddingExecutor所執行的,下面我們馬上看看這個執行者的代碼。

BufferPaddingExecutor - RingBuffer元素填充器

該用於填充RingBuffer的執行者最主要的執行方法如下

/** * Padding buffer fill the slots until to catch the cursor * <p> * 該方法被即時填充和定期填充所調用 */
    public void paddingBuffer() {
        LOGGER.info("{} Ready to padding buffer lastSecond:{}. {}", this, lastSecond.get(), ringBuffer);

        // is still running
        // 這個是代表填充executor在執行,不是RingBuffer在執行。爲免多個線程同時擴容。
        if (!running.compareAndSet(false, true)) {
            LOGGER.info("Padding buffer is still running. {}", ringBuffer);
            return;
        }

        // fill the rest slots until to catch the cursor
        boolean isFullRingBuffer = false;
        while (!isFullRingBuffer) {
            // 填充完指定SECOND裏面的所有UID,直至填滿
            List<Long> uidList = uidProvider.provide(lastSecond.incrementAndGet());
            for (Long uid : uidList) {
                isFullRingBuffer = !ringBuffer.put(uid);
                if (isFullRingBuffer) {
                    break;
                }
            }
        }

        // not running now
        running.compareAndSet(true, false);
        LOGGER.info("End to padding buffer lastSecond:{}. {}", lastSecond.get(), ringBuffer);
    }

當線程池分發多條線程來執行填充任務的時候,成功搶奪運行狀態的線程會真正執行對RingBuffer填充,直至全部填滿,其他搶奪失敗的線程將會直接返回。

  1. 該類還提供定時填充功能,如果有設置開關則會生效,默認不會啓用週期性填充。
 /** * Start executors such as schedule */
    public void start() {
        if (bufferPadSchedule != null) {
            bufferPadSchedule.scheduleWithFixedDelay(this::paddingBuffer, scheduleInterval, scheduleInterval, TimeUnit.SECONDS);
        }
    }
  1. 在take()方法中檢測到達到填充閾值時,會進行異步填充。
 /** * Padding buffer in the thread pool */
    public void asyncPadding() {
        bufferPadExecutors.submit(this::paddingBuffer);
    }

其他函數式接口

BufferedUidProvider- UID的提供者,在本倉庫中以lambda形式出現在com.baidu.fsg.uid.impl.CachedUidGenerator#nextIdsForOneSecond
RejectedPutBufferHandler- 當RingBuffer滿時拒絕繼續添加的處理者,在本倉庫中的表現形式爲com.baidu.fsg.uid.buffer.RingBuffer#discardPutBuffer
RejectedTakeBufferHandler- 當RingBuffer爲空時拒絕獲取UID的處理者,在本倉庫中的表現形式爲com.baidu.fsg.uid.buffer.RingBuffer#exceptionRejectedTakeBuffer

CachedUidGenerator - 使用RingBuffer的UID生成器

該類在應用中作爲Spring Bean注入到各個組件中,主要作用是初始化RingBufferBufferPaddingExecutor。獲取ID是通過委託RingBuffer的take()方法達成的,而最重要的方法爲BufferedUidProvider的提供者,即lambda表達式中的nextIdsForOneSecond(long)方法

/** * Get the UIDs in the same specified second under the max sequence * * @param currentSecond * @return UID list, size of {@link BitsAllocator#getMaxSequence()} + 1 */
    protected List<Long> nextIdsForOneSecond(long currentSecond) {
        // Initialize result list size of (max sequence + 1)
        int listSize = (int) bitsAllocator.getMaxSequence() + 1;
        List<Long> uidList = new ArrayList<>(listSize);

        // Allocate the first sequence of the second, the others can be calculated with the offset
        long firstSeqUid = bitsAllocator.allocate(currentSecond - epochSeconds, workerId, 0L);
        for (int offset = 0; offset < listSize; offset++) {
            uidList.add(firstSeqUid + offset);
        }

        return uidList;
    }

用於生成指定秒currentSecond內的全部UID,提供給BufferPaddingExecutor進行填充。

總結

  1. RIngBuffer的填充時機有3個:CachedUidGenerator時對RIngBuffer初始化、RIngBuffer#take()時檢測達到閾值和週期性填充(如果有打開)。
  2. RingBuffer的slots數組多讀少寫,不考慮僞共享問題。
  3. JDK8中-XX:-RestrictContended搭配@sun.misc.Contended
    請下載代碼

參考資料:


1.百度uid-generator源碼

2.百度 - DLock源碼解析

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章