百度分佈式id生產器UidGenerator

前言

UidGenerator是百度開源的Java語言實現,基於Snowflake算法的惟一ID生成器。並且,它很是適合虛擬環境,好比:Docker。另外,它經過消費將來時間克服了雪花算法的併發限制。UidGenerator提早生成ID並緩存在RingBuffer中。 壓測結果顯示,單個實例的QPS能超過6000,000。依賴環境:node

  • JDK8+
  • MySQL(用於分配WorkerId)

snowflake

雪花算法幾個核心部分: 算法

  • 1位sign標識位;
  • 41位時間戳;
  • 10位workId(數據中心+工做機器,能夠其餘組成方式);
  • 12位自增序列;

百度分佈式id生成器作了修改: 數據庫

時間部分是28位,意味着默認只能承受8.5年(2^28-1/86400/365)。根據不一樣業務需求,能夠適當調整delta seconds,worker node id和sequence佔用位數。 UidGenerator提供兩種方式:DefaultUidGenerator 和 CachedUidGenerator 。數組

DefaultUidGenerator

delta seconds緩存

指的是當前時間和epoch時間的時間差,單位位秒。epoch時間指的是UidGenerator生成分佈式ID服務第一次上線的時間,可配置,也必定要根據你的上線時間進行配置,由於默認的epoch時間但是2016-09-20,不配置的話,會浪費好幾年的可用時間。安全

worker id服務器

看下worker id是如何賦值的,先建立一個表:數據結構

DROP TABLE IF EXISTS WORKER_NODE;
CREATE TABLE WORKER_NODE(
  ID BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY ,
  HOST_NAME VARCHAR(64) NOT NULL COMMENT 'host name',
  PORT VARCHAR(64) NOT NULL COMMENT 'port',
  TYPE INT NOT NULL COMMENT 'node type: ACTUAL or CONTAINER',
  LAUNCH_DATE DATE NOT NULL COMMENT 'launch date',
  MODIFIED DATETIME NOT NULL COMMENT 'modified time',
  CREATED DATEIMTE NOT NULL COMMENT 'created time'
)
 COMMENT='DB WorkerID Assigner for UID Generator',ENGINE = INNODB;

分佈式ID的實例啓動的時候,往這個表中插入一行數據,獲得的id值就是準備賦給workerId的值。因爲workerId默認22位,那麼,集成UidGenerator生成分佈式ID的全部實例重啓次數是不容許超過4194303次(即2^22-1),不然會拋出異常。 固然也能夠自定義生成workerid的方式。併發

**sequence **異步

關鍵點實現:

  • synchronized保證線程安全;
  • 若是時間有任何的回撥,那麼直接拋出異常;
  • 若是當前時間和上一次是同一秒時間,那麼sequence自增。若是同一秒內自增值超過2^13-1,那麼就會自旋等待下一秒(getNextSecond);
  • 若是是新的一秒,那麼sequence從新從0開始;
protected synchronized long nextId() {
    long currentSecond = getCurrentSecond();
    if (currentSecond < lastSecond) {
        long refusedSeconds = lastSecond - currentSecond;
        throw new UidGenerateException("Clock moved backwards. Refusing for %d seconds", refusedSeconds);
    }
    if (currentSecond == lastSecond) {
        sequence = (sequence + 1) & bitsAllocator.getMaxSequence();
        if (sequence == 0) {
            currentSecond = getNextSecond(lastSecond);
        }
    } else {
        sequence = 0L;
    }
    lastSecond = currentSecond;
    return bitsAllocator.allocate(currentSecond - epochSeconds, workerId, sequence);
}

總結

時鐘回撥的處理比較簡單粗暴。另外若是使用UidGenerator的DefaultUidGenerator方式生成分佈式ID,必定要根據你的業務的狀況和特色,調整各個字段佔用的位數:

<property name="timeBits" value="28"/>
<property name="workerBits" value="22"/>
<property name="seqBits" value="13"/>
<property name="epochStr" value="2016-09-20"/>

CachedUidGenerator

CachedUidGenerator是UidGenerator的重要改進實現。它的核心利用了RingBuffer,以下圖所示,它本質上是一個數組,數組中每一個項被稱爲slot。UidGenerator設計了兩個RingBuffer,一個保存惟一ID,一個保存flag。RingBuffer的尺寸是2^n,n必須是正整數:

RingBuffer Of Flag

保存flag這個RingBuffer的每一個slot的值都是0或者1,0是CAN_PUT_FLAG的標誌位,1是CAN_TAKE_FLAG的標識位。每一個slot的狀態要麼是CAN_PUT,要麼是CAN_TAKE。以某個slot的值爲例,初始值爲0,即CAN_PUT。接下來會初始化填滿這個RingBuffer,這時候這個slot的值就是1,即CAN_TAKE。等獲取分佈式ID時取到這個slot的值後,這個slot的值又變爲0,以此類推。

RingBuffer Of UID

保存惟一ID的RingBuffer有兩個指針,Tail指針和Cursor指針。

Tail指針表示最後一個生成的惟一ID。若是這個指針追上了Cursor指針,意味着RingBuffer已經滿了。這時候,不容許再繼續生成ID了。用戶能夠經過屬性rejectedPutBufferHandler指定處理這種狀況的策略。

Cursor指針表示最後一個已經給消費的惟一ID。若是Cursor指針追上了Tail指針,意味着RingBuffer已經空了。這時候,不容許再繼續獲取ID了。用戶能夠經過屬性rejectedTakeBufferHandler指定處理這種異常狀況的策略。

另外,若是你想加強RingBuffer提高它的吞吐能力,那麼須要配置一個更大的boostPower值:

<!-- RingBuffer size擴容參數, 可提升UID生成能力.即每秒產生ID數上限能力 --> 
<!-- 默認:3,原bufferSize=2^13, 擴容後bufferSize = 2^13 << 3 = 65536 -->
<property name="boostPower" value="3"/>

CachedUidGenerator的理論講完後,接下來就是它具體是如何實現的了,咱們首先看它的申明,它是實現了DefaultUidGenerator,因此,它事實上就是對DefaultUidGenerator的加強:

public class CachedUidGenerator extends DefaultUidGenerator implements DisposableBean {
   ... ...
}

worker id

CachedUidGenerator的workerId實現繼承自它的父類DefaultUidGenerator,即實例啓動時往表WORKER_NODE插入數據後獲得的自增ID值。

接下來深刻解讀CachedUidGenerator的核心操做,即對RingBuffer的操做,包括初始化、取分佈式惟一ID、填充分佈式惟一ID等。

初始化

CachedUidGenerator在初始化時除了給workerId賦值,還會初始化RingBuffer。這個過程主要工做有:

  • 根據boostPower的值肯定RingBuffer的size;
  • 構造RingBuffer,默認paddingFactor爲50。這個值的意思是當RingBuffer中剩餘可用ID數量少於50%的時候,就會觸發一個異步線程往RingBuffer中填充新的惟一ID(調用BufferPaddingExecutor中的paddingBuffer()方法,這個線程中會有一個標誌位running控制併發問題),直到填滿爲止;
  • 判斷是否配置了屬性scheduleInterval,這是另一種RingBuffer填充機制, 在Schedule線程中, 週期性檢查填充。默認:不配置, 即不使用Schedule線程. 如需使用, 請指定Schedule線程時間間隔, 單位:秒;
  • 初始化Put操做拒絕策略,對應屬性rejectedPutBufferHandler。即當RingBuffer已滿, 沒法繼續填充時的操做策略。默認無需指定, 將丟棄Put操做, 僅日誌記錄. 若有特殊需求, 請實現RejectedPutBufferHandler接口(支持Lambda表達式);
  • 初始化Take操做拒絕策略,對應屬性rejectedTakeBufferHandler。即當環已空, 沒法繼續獲取時的操做策略。默認無需指定, 將記錄日誌, 並拋出UidGenerateException異常. 若有特殊需求, 請實現RejectedTakeBufferHandler接口;
  • 初始化填滿RingBuffer中全部slot(即塞滿惟一ID,這一步和第2步驟同樣都是調用BufferPaddingExecutor中的paddingBuffer()方法);
  • 開啓buffer補丁線程(前提是配置了屬性scheduleInterval),原理就是利用ScheduledExecutorService的scheduleWithFixedDelay()方法。

說明:第二步的異步線程實現很是重要,也是UidGenerator解決時鐘回撥的關鍵:在知足填充新的惟一ID條件時,經過時間值遞增獲得新的時間值(lastSecond.incrementAndGet()),而不是System.currentTimeMillis()這種方式,而lastSecond是AtomicLong類型,因此能保證線程安全問題。

取值

RingBuffer初始化有值後,接下來的取值就簡單了。不過,因爲分佈式ID都保存在RingBuffer中,取值過程當中就會有一些邏輯判斷:

  • 若是剩餘可用ID百分比低於paddingFactor參數指定值,就會異步生成若干個ID集合,直到將RingBuffer填滿。
  • 若是獲取值的位置追上了tail指針,就會執行Task操做的拒絕策略。
  • 獲取slot中的分佈式ID。
  • 將這個slot的標誌位只爲CAN_PUT_FLAG。

總結

經過上面對UidGenerator的分析可知,CachedUidGenerator方式主要經過採起以下一些措施和方案規避了時鐘回撥問題和加強惟一性:

  • 自增列:UidGenerator的workerId在實例每次重啓時初始化,且就是數據庫的自增ID,從而完美的實現每一個實例獲取到的workerId不會有任何衝突。
  • RingBuffer:UidGenerator再也不在每次取ID時都實時計算分佈式ID,而是利用RingBuffer數據結構預先生成若干個分佈式ID並保存。
  • 時間遞增:傳統的雪花算法實現都是經過System.currentTimeMillis()來獲取時間並與上一次時間進行比較,這樣的實現嚴重依賴服務器的時間。而UidGenerator的時間類型是AtomicLong,且經過incrementAndGet()方法獲取下一次的時間,從而脫離了對服務器時間的依賴,也就不會有時鐘回撥的問題(這種作法也有一個小問題,即分佈式ID中的時間信息可能並非這個ID真正產生的時間點,例如:獲取的某分佈式ID的值爲3200169789968523265,它的反解析結果爲{"timestamp":"2019-05-02 23:26:39","workerId":"21","sequence":"1"},可是這個ID可能並非在"2019-05-02 23:26:39"這個時間產生的)。
相關文章
相關標籤/搜索