EventTransactionBuffer中的ringBuffer詳解

簡介

parser模塊下,AbstractEventParser類中的定時任務不斷的向Mysql的Master節點發送dump()請求,接收binlog數據,把接收到的binlog日誌數據存儲在一個大小爲1024的ringBuffer中,然後再flush到sink模塊,本文將對ringbuffer的操作流程做一個梳理。

核心流程

Ringbuffer的核心包含幾個變量

// 數據實際存儲的數組
  private CanalEntry.Entry[]       entries;
// 代表當前put操作最後一次寫操作發生的位置
    private AtomicLong               putSequence   = new AtomicLong(INIT_SQEUENCE); 
// 代表滿足flush條件後最後一次數據flush的時間
    private AtomicLong               flushSequence = new AtomicLong(INIT_SQEUENCE); 

 private static final long        INIT_SQEUENCE = -1;
  private int                      bufferSize    = 1024;

從上文可以看出,ringbuffer實際上是一個數組entries,下標從0開始,數組大小爲1024,通過putSequence和flushSequence兩個變量(初始化爲-1),可以把這個數組作爲一個環來使用

初始狀態

初始狀態下,putSequence和flushSequence都爲-1

put操作

put操作,會向ringbuffer中添加一個元素,如下圖所示:

上圖是給ringbuffer下標爲0的位置添加了一個元素,put操作的流程如下:

  1. 檢測是否有空位
  2. 如果有空位則進行如下操作
    1. 先給數組裏添加值
    2. putSequence+1
  3. 如果沒有空位,則進行如下操作:
    1. 執行flush()
    2. 重新執行put()

源碼如下:

private void put(CanalEntry.Entry data) throws InterruptedException {
        // 首先檢查是否有空位
        if (checkFreeSlotAt(putSequence.get() + 1)) {
            long current = putSequence.get();
            long next = current + 1;

            // 先寫數據,再更新對應的cursor,併發度高的情況,putSequence會被get請求可見,拿出了ringbuffer中的老的Entry值
            entries[getIndex(next)] = data;
            putSequence.set(next);
        } else {
            flush();// buffer區滿了,刷新一下
            put(data);// 繼續加一下新數據
        }
    }

查詢是否有空位的代碼如下:

private boolean checkFreeSlotAt(final long sequence) {
        final long wrapPoint = sequence - bufferSize;
        if (wrapPoint > flushSequence.get()) { // 剛好追上一輪
            return false;
        } else {
            return true;
        }
    }

下圖是put 3次之後的ringbuffer

flush操作

flush操作會把ringbuffer中的數據一次性全部推送出去,根據上文描述,flush之前ringbuffer中有3條數據,flush之後會全部推送出去,推送後的數組如下圖所示

flush源碼如下

private void flush() throws InterruptedException {
        long start = this.flushSequence.get() + 1;
        long end = this.putSequence.get();

        if (start <= end) {
            List<CanalEntry.Entry> transaction = new ArrayList<>();
            for (long next = start; next <= end; next++) {
                transaction.add(this.entries[getIndex(next)]);
            }

            flushCallback.flush(transaction);
            flushSequence.set(end);// flush成功後,更新flush位置
        }
    }

flushCallback.flush(transaction) 其實就是在調用sink模塊

ringbuffer資源的重複利用機制

上文中提到,ringbuffer實際上是一個數組,數組本身是沒有首尾相連的,canal通過一定的機制將數組變成了一個邏輯上的圓環,核心邏輯就是getInex()方法:

private int getIndex(long sequcnce) {
  	return (int) sequcnce & indexMask;
}

該方法中,ringbuffer大小爲1024,因此indexMask=1023,如上圖所示,當給entries中put數據時,會先調用getIndex()獲取當前操作應該使用的下標位置,

比如,當前sequcnce=1023時,實際上已經到了數組的最後一個位置,下一個序列是1024,它對應的下標是1024& 1023=0,

也就是下標爲0的位置,而此時flushSequence位於下標爲2的數組位置,表明數組還沒有滿,下標爲0的位置是空的,可以使用,如此則實現了ringbuffer的資源重複利用

需要注意的是

  1. ringbuffer的大小必須是2^n,否則無法實現循環。

  2. flush操作只是改變了flushSequence的值,events數組中的數據並沒有被清理,因此events數組在第一次滿了之後會一直處於滿了的狀態,只是會不斷被覆蓋。

Ringbuffer滿了以後如何操作?

如下圖所示,當ringbuffer中已經存滿了之後,新的數據1027無法再添加

此時會先調用一次flush(),將數據推送出去,然後再將1027添加到隊列,如下圖所示

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