百度 - UidGenerator源碼解析
簡介
UidGenerator是Java實現的,基於Snowflake算法的唯一ID生成器。UidGenerator以組件形式工作在應用項目中,支持自定義workerId位數和初始化策略,從而適用於Docker等虛擬化環境下實例自動重啓、漂移等場景。 在實現上,UidGenerator通過採用RingBuffer來緩存已生成的UID,並行化UID的生產和消費,同時對CacheLine補齊,避免了由RingBuffer帶來的硬件級「僞共享」問題。
snowflake算法
uid-generator是基於Twitter開源的snowflake算法實現的。
snowflake將long的64位分爲了3部分,時間戳、工作機器id和序列號,位數分配如下。
其中,時間戳部分的時間單位一般爲毫秒。也就是說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位由deltaSeconds
、workerId
和sequence
組成,注意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填充,直至全部填滿,其他搶奪失敗的線程將會直接返回。
- 該類還提供定時填充功能,如果有設置開關則會生效,默認不會啓用週期性填充。
/** * Start executors such as schedule */
public void start() {
if (bufferPadSchedule != null) {
bufferPadSchedule.scheduleWithFixedDelay(this::paddingBuffer, scheduleInterval, scheduleInterval, TimeUnit.SECONDS);
}
}
- 在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注入到各個組件中,主要作用是初始化RingBuffer
和BufferPaddingExecutor
。獲取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
進行填充。
總結
- RIngBuffer的填充時機有3個:CachedUidGenerator時對RIngBuffer初始化、RIngBuffer#take()時檢測達到閾值和週期性填充(如果有打開)。
- RingBuffer的slots數組多讀少寫,不考慮僞共享問題。
- JDK8中
-XX:-RestrictContended
搭配@sun.misc.Contended
。
請下載代碼
參考資料: