從零開始實現lmax-Disruptor佇列(二)多消費者、消費者組間消費依賴原理解析

小熊餐館發表於2022-06-09

MyDisruptor V2版本介紹

在v1版本的MyDisruptor實現單生產者、單消費者功能後。按照計劃,v2版本的MyDisruptor需要支援多消費者和允許設定消費者組間的依賴關係。

由於該文屬於系列部落格的一部分,需要先對之前的部落格內容有所瞭解才能更好地理解本篇部落格

MyDisruptor支援多消費者

  • disruptor中的生產者和消費者是互相制約的,生產者的生產速度不能過快,在邏輯上佇列已滿時需要阻塞等待消費者進行消費,直到佇列不滿。
  • 而要支援多消費者,上述描述需要做一定的調整:即生產者的生產速度不能過快,在邏輯上佇列已滿時需要阻塞等待”最慢的消費者“完成消費,直到佇列不滿。
  • disruptor中每個消費者都擁有自己的消費序列號,生產者在生產時需要保證生產的序列號不能覆蓋任何一個消費者,即生產者的序列號不能超過最慢的消費者序列號一圈(Producer.Sequence - SlowestConsumer.Sequence <= ringBufferSize)
    所以生產者需要維護一個消費者序列集合,在生產者生產時控制生產速度避免超過最慢的消費者而發生越界。
v2版本單執行緒生產者實現
package mydisruptor;

import mydisruptor.util.SequenceUtil;
import mydisruptor.waitstrategy.MyWaitStrategy;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.locks.LockSupport;

/**
 * 單執行緒生產者序列器(仿Disruptor.SingleProducerSequencer)
 * 只支援單消費者的簡易版本(只有一個consumerSequence)
 *
 * 因為是單執行緒序列器,因此在設計上就是執行緒不安全的
 * */
public class MySingleProducerSequencer {

    /**
     * 生產者序列器所屬ringBuffer的大小
     * */
    private final int ringBufferSize;

    /**
     * 當前已釋出的生產者序列號
     * (區別於nextValue)
     * */
    private final MySequence currentProducerSequence = new MySequence();

    /**
     * 生產者序列器所屬ringBuffer的消費者序列集合
     * (v2版本簡單起見,先不和disruptor一樣用陣列+unsafe來實現)
     * */
    private final List<MySequence> gatingConsumerSequenceList = new ArrayList<>();

    private final MyWaitStrategy myWaitStrategy;

    /**
     * 當前已申請的序列(但是是否釋出了,要看currentProducerSequence)
     *
     * 單執行緒生產者內部使用,所以就是普通的long,不考慮併發
     * */
    private long nextValue = -1;

    /**
     * 當前已快取的消費者序列
     *
     * 單執行緒生產者內部使用,所以就是普通的long,不考慮併發
     * */
    private long cachedConsumerSequenceValue = -1;

    public MySingleProducerSequencer(int ringBufferSize, MyWaitStrategy myWaitStrategy) {
        this.ringBufferSize = ringBufferSize;
        this.myWaitStrategy = myWaitStrategy;
    }

    /**
     * 一次性申請可用的1個生產者序列號
     * */
    public long next(){
        return next(1);
    }

    /**
     * 一次性申請可用的n個生產者序列號
     * */
    public long next(int n){
        // 申請的下一個生產者位點
        long nextProducerSequence = this.nextValue + n;
        // 新申請的位點下,生產者恰好超過消費者一圈的環繞臨界點序列
        long wrapPoint = nextProducerSequence - this.ringBufferSize;

        // 獲得當前已快取的消費者位點
        long cachedGatingSequence = this.cachedConsumerSequenceValue;

        // 消費者位點cachedValue並不是實時獲取的(因為在沒有超過環繞點一圈時,生產者是可以放心生產的)
        // 每次釋出都實時獲取反而會觸發對消費者sequence強一致的讀,迫使消費者執行緒所在的CPU重新整理快取(而這是不需要的)
        if(wrapPoint > cachedGatingSequence){
            // 比起disruptor省略了if中的cachedGatingSequence > nextProducerSequence邏輯
            // 原因請見:https://github.com/LMAX-Exchange/disruptor/issues/76

            // 比起disruptor省略了currentProducerSequence.set(nextProducerSequence);
            // 原因請見:https://github.com/LMAX-Exchange/disruptor/issues/291
            long minSequence;

            // 當生產者發現確實當前已經超過了一圈,則必須去讀最新的消費者序列了,看看消費者的消費進度是否推進了
            // 這裡的getMinimumSequence方法中是對volatile變數的讀,是實時的、強一致的讀
            while(wrapPoint > (minSequence = SequenceUtil.getMinimumSequence(nextProducerSequence, gatingConsumerSequenceList))){
                // 如果確實超過了一圈,則生產者無法獲取可用的佇列空間,迴圈的間歇性park阻塞
                LockSupport.parkNanos(1L);
            }

            // 滿足條件了,則快取獲得最新的消費者序列
            // 因為不是實時獲取消費者序列,可能cachedValue比上一次的值要大很多
            // 這種情況下,待到下一次next申請時就可以不用去強一致的讀consumerSequence了
            this.cachedConsumerSequenceValue = minSequence;
        }

        // 記錄本次申請後的,已申請的生產者位點
        this.nextValue = nextProducerSequence;

        return nextProducerSequence;
    }

    public void publish(long publishIndex){
        // 釋出時,更新生產者佇列
        // lazySet,由於消費者可以批量的拉取資料,所以不必每次釋出時都volatile的更新,允許消費者晚一點感知到,這樣效能會更好
        // 設定寫屏障
        this.currentProducerSequence.lazySet(publishIndex);

        // 釋出完成後,喚醒可能阻塞等待的消費者執行緒
        this.myWaitStrategy.signalWhenBlocking();
    }

    public MySequenceBarrier newBarrier(){
        return new MySequenceBarrier(this.currentProducerSequence,this.myWaitStrategy,new ArrayList<>());
    }

    public MySequenceBarrier newBarrier(MySequence... dependenceSequences){
        return new MySequenceBarrier(this.currentProducerSequence,this.myWaitStrategy,new ArrayList<>(Arrays.asList(dependenceSequences)));
    }

    public void addGatingConsumerSequenceList(MySequence newGatingConsumerSequence){
        this.gatingConsumerSequenceList.add(newGatingConsumerSequence);
    }

    public int getRingBufferSize() {
        return ringBufferSize;
    }
}

/**
 * 序列號工具類
 * */
public class SequenceUtil {

    /**
     * 從依賴的序列集合dependentSequence和申請的最小序列號minimumSequence中獲得最小的序列號
     * @param minimumSequence 申請的最小序列號
     * @param dependentSequenceList 依賴的序列集合
     * */
    public static long getMinimumSequence(long minimumSequence, List<MySequence> dependentSequenceList){
        for (MySequence sequence : dependentSequenceList) {
            long value = sequence.get();
            minimumSequence = Math.min(minimumSequence, value);
        }

        return minimumSequence;
    }

    /**
     * 獲得傳入的序列集合中最小的一個序列號
     * @param dependentSequenceList 依賴的序列集合
     * */
    public static long getMinimumSequence(List<MySequence> dependentSequenceList){
        // Long.MAX_VALUE作為上界,即使dependentSequenceList為空,也會返回一個Long.MAX_VALUE作為最小序列號
        return getMinimumSequence(Long.MAX_VALUE, dependentSequenceList);
    }
}
  • v2版本生產者相對於v1版本的一個變化就是將維護的單一消費者序列consumerSequence變為了一個容納多消費者序列的集合gatingConsumerSequenceList,並提供了動態新增消費者序列的介面addGatingConsumerSequenceList方法。
  • 在申請可用生產序列號的方法next中,判斷是否越界的條件也由v1版本的wrapPoint > consumerSequence,變成了wrapPoint > SequenceUtil.getMinimumSequence()。
  • SequenceUtil.getMinimumSequence方法接收一個序列號集合和一個生產者序列號,返回其中的最小序列值。如果環繞越界點序列大於了返回的最小序列值,則說明所要申請的序列號已經越界了(快於最慢消費者一圈),需要等待最慢的消費者消費,令生產者阻塞。

MyDisruptor支援消費者組間消費依賴

  • v2版本中除了要支援多消費者,還需要支援消費者的組間消費依賴,例如有三個消費者A、B、C,消費依賴關係為A/B -> C, 即對於生產者釋出的任意一個事件在AB消費成功後C才能進行消費(A,B之間的消費順序不限制)。
  • 消費者的組間消費依賴關係可以很複雜(但不能存在迴圈依賴)。
  • 要實現消費者間的依賴,關鍵思路是讓每個消費者維護其上游消費者的序列,在消費時控制所消費的序列號不大於上游所依賴的最慢的消費者
/**
 * 序列柵欄(仿Disruptor.SequenceBarrier)
 * */
public class MySequenceBarrier {

    private final MySequence currentProducerSequence;
    private final MyWaitStrategy myWaitStrategy;
    private final List<MySequence> dependentSequencesList;

    public MySequenceBarrier(MySequence currentProducerSequence,
                             MyWaitStrategy myWaitStrategy, List<MySequence> dependentSequencesList) {
        this.currentProducerSequence = currentProducerSequence;
        this.myWaitStrategy = myWaitStrategy;
        this.dependentSequencesList = dependentSequencesList;
    }

    /**
     * 獲得可用的消費者下標
     * */
    public long getAvailableConsumeSequence(long currentConsumeSequence) throws InterruptedException {
        // v1版本只是簡單的呼叫waitFor,等待其返回即可
        return this.myWaitStrategy.waitFor(currentConsumeSequence,currentProducerSequence,dependentSequencesList);
    }
}

/**
 * 阻塞等待策略
 * */
public class MyBlockingWaitStrategy implements MyWaitStrategy{

    private final Lock lock = new ReentrantLock();
    private final Condition processorNotifyCondition = lock.newCondition();

    @Override
    public long waitFor(long currentConsumeSequence, MySequence currentProducerSequence, List<MySequence> dependentSequences)
            throws InterruptedException {
        // 強一致的讀生產者序列號
        if (currentProducerSequence.get() < currentConsumeSequence) {
            // 如果ringBuffer的生產者下標小於當前消費者所需的下標,說明目前消費者消費速度大於生產者生產速度

            lock.lock();
            try {
                //
                while (currentProducerSequence.get() < currentConsumeSequence) {
                    // 消費者的消費速度比生產者的生產速度快,阻塞等待
                    processorNotifyCondition.await();
                }
            }
            finally {
                lock.unlock();
            }
        }

        // 跳出了上面的迴圈,說明生產者序列已經超過了當前所要消費的位點(currentProducerSequence > currentConsumeSequence)
        long availableSequence;
        if(!dependentSequences.isEmpty()){
            // 受制於屏障中的dependentSequences,用來控制當前消費者消費進度不得超過其所依賴的鏈路上游的消費者進度
            while ((availableSequence = SequenceUtil.getMinimumSequence(dependentSequences)) < currentConsumeSequence) {
                // 由於消費者消費速度一般會很快,所以這裡使用自旋阻塞來等待上游消費者進度推進(響應及時,且實現簡單)

                // 在jdk9開始引入的Thread.onSpinWait方法,優化自旋效能
                MyThreadHints.onSpinWait();
            }
        }else{
            // 並不存在依賴的上游消費者,大於當前消費進度的生產者序列就是可用的消費序列
            availableSequence = currentProducerSequence.get();
        }

        return availableSequence;
    }

    @Override
    public void signalWhenBlocking() {
        lock.lock();
        try {
            // signal喚醒所有阻塞在條件變數上的消費者執行緒(後續支援多消費者時,會改為signalAll)
            processorNotifyCondition.signal();
        }
        finally {
            lock.unlock();
        }
    }
}
  • v2版本中,控制消費者消費速度的元件,MySequenceBarrier序列屏障中除了需要維護生產者的序列號,避免消費越界外,還新增了一個List型別的成員變數dependentSequencesList用於維護所依賴的上游的一至多個消費者的序列號物件。
  • 獲取可用的最大消費者序列號方法MyWaitStrategy.waitFor方法也新增引數傳入依賴的上游消費者序列集合,用於控制返回的最大可消費序列號不會大於上游最慢的消費者序列。
  • 在阻塞等待策略MyBlockingWaitStrategy中,和等待生產者生產時的策略不同,等待上游最慢消費者消費時並不是通過Condition.await方法令執行緒陷入阻塞態,而是while自旋等待。
    這是因為生產者地下一次生產是不可預知的,有可能會長時間等待;比起自旋,阻塞等待可以極大地減少對CPU資源的消耗。而上游消費者的消費速度則一般很快,生產者生產到上游消費者消費完之間的時間間隔會很短,所以使用自旋來實現消費者間的消費依賴
    注意:正是因為在消費者的實現中假設了上游消費者的“理論”消費速度是很快的。所以在實際使用時,使用者自定義的消費邏輯中不應該出現耗時的操作(考慮非同步化),否則將可能導致下游的消費者陷入長時間的自旋中浪費大量的CPU資源

MyThreadHints.onSpinWait分析

在waitFor方法中自旋並不是簡單的空迴圈,而是呼叫了MyThreadHints.onSpinWait方法。

/**
 * 啟發性的查詢是否存在Thread.onSpinWait方法,如果有則可以呼叫,如果沒有則執行空邏輯
 *
 * 相容老版本無該方法的jdk(Thread.onSpinWait是jdk9開始引入的)
 * */
public class MyThreadHints {

    private static final MethodHandle ON_SPIN_WAIT_METHOD_HANDLE;

    static {
        final MethodHandles.Lookup lookup = MethodHandles.lookup();

        MethodHandle methodHandle = null;
        try {
            methodHandle = lookup.findStatic(Thread.class, "onSpinWait", methodType(void.class));
        } catch (final Exception ignore) {
            // jdk9才引入的Thread.onSpinWait, 低版本沒找到該方法直接忽略異常即可
        }

        ON_SPIN_WAIT_METHOD_HANDLE = methodHandle;
    }

    public static void onSpinWait() {
        // Call java.lang.Thread.onSpinWait() on Java SE versions that support it. Do nothing otherwise.
        // This should optimize away to either nothing or to an inlining of java.lang.Thread.onSpinWait()
        if (null != ON_SPIN_WAIT_METHOD_HANDLE) {
            try {
                // 如果是高版本jdk找到了Thread.onSpinWait方法,則進行呼叫, 插入特殊指令優化CPU自旋效能(例如x86架構中的pause彙編指令)
                // invokeExact比起反射呼叫方法要高一些,詳細的原因待研究
                ON_SPIN_WAIT_METHOD_HANDLE.invokeExact();
            }
            catch (final Throwable ignore) {
                // 異常無需考慮
            }
        }
    }
}
  • MyThreadHints內部在static塊中通過MethodHandles嘗試著獲取Thread類的靜態方法onSpinWait。這一方法在jdk9中被引入,因此在包括jdk8在內的低版本jdk中,都無法找到該方法,初始化完後ON_SPIN_WAIT_METHOD_HANDLE會是null。
  • jdk7開始引入的MethodHandles中,由於提前進行了很多的合法性檢查,比常見的反射呼叫方式效率要高一些(但比起反射缺失了一些好用的功能)。所以在追求極致效能的disruptor中,用於自旋中的邏輯自然選擇了效率更高的MethodHandles。
  • jdk9開始引入的Thread.onSpinWait中,一般是執行特殊的彙編指令用於告訴CPU當前方法處於無意義的自旋等待中,讓CPU進行一定的優化。具體的實現主要取決於底層具體的硬體平臺型別,如x86架構下是pause指令。
    關於pause指令優化CPU自旋效能損耗的原理涉及到過多的硬體知識,限於個人水平就不再展開了。

MyDisruptor v2版本demo解析

v2版本支援了多生產者和消費者組間消費依賴的功能,下面通過一個稍顯複雜的demo來展示如何使用這些功能。

public class MyRingBufferV2Demo {

    /**
     * 樹形依賴關係
     * A,B->C->E
     *    ->D->F,G
     * */
    public static void main(String[] args) throws InterruptedException {
        // 環形佇列容量為16(2的4次方)
        int ringBufferSize = 16;

        // 建立環形佇列
        MyRingBuffer<OrderEventModel> myRingBuffer = MyRingBuffer.createSingleProducer(
                new OrderEventProducer(), ringBufferSize, new MyBlockingWaitStrategy());

        // 獲得ringBuffer的序列屏障(最上游的序列屏障內只維護生產者的序列)
        MySequenceBarrier mySequenceBarrier = myRingBuffer.newBarrier();

        // ================================== 基於生產者序列屏障,建立消費者A
        MyBatchEventProcessor<OrderEventModel> eventProcessorA =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerA"), mySequenceBarrier);
        MySequence consumeSequenceA = eventProcessorA.getCurrentConsumeSequence();
        // RingBuffer監聽消費者A的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceA);

        // ================================== 基於生產者序列屏障,建立消費者B
        MyBatchEventProcessor<OrderEventModel> eventProcessorB =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerB"), mySequenceBarrier);
        MySequence consumeSequenceB = eventProcessorB.getCurrentConsumeSequence();
        // RingBuffer監聽消費者B的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceB);

        // ================================== 消費者C依賴上游的消費者A,B,通過消費者A、B的序列號建立序列屏障(構成消費的順序依賴)
        MySequenceBarrier mySequenceBarrierC = myRingBuffer.newBarrier(consumeSequenceA,consumeSequenceB);
        // 基於序列屏障,建立消費者C
        MyBatchEventProcessor<OrderEventModel> eventProcessorC =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerC"), mySequenceBarrierC);
        MySequence consumeSequenceC = eventProcessorC.getCurrentConsumeSequence();
        // RingBuffer監聽消費者C的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceC);

        // ================================== 消費者E依賴上游的消費者C,通過消費者C的序列號建立序列屏障(構成消費的順序依賴)
        MySequenceBarrier mySequenceBarrierE = myRingBuffer.newBarrier(consumeSequenceC);
        // 基於序列屏障,建立消費者E
        MyBatchEventProcessor<OrderEventModel> eventProcessorE =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerE"), mySequenceBarrierE);
        MySequence consumeSequenceE = eventProcessorE.getCurrentConsumeSequence();
        // RingBuffer監聽消費者E的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceE);

        // ================================== 消費者D依賴上游的消費者A,B,通過消費者A、B的序列號建立序列屏障(構成消費的順序依賴)
        MySequenceBarrier mySequenceBarrierD = myRingBuffer.newBarrier(consumeSequenceA,consumeSequenceB);
        // 基於序列屏障,建立消費者D
        MyBatchEventProcessor<OrderEventModel> eventProcessorD =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerD"), mySequenceBarrierD);
        MySequence consumeSequenceD = eventProcessorD.getCurrentConsumeSequence();
        // RingBuffer監聽消費者D的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceD);

        // ================================== 消費者F依賴上游的消費者D,通過消費者D的序列號建立序列屏障(構成消費的順序依賴)
        MySequenceBarrier mySequenceBarrierF = myRingBuffer.newBarrier(consumeSequenceD);
        // 基於序列屏障,建立消費者F
        MyBatchEventProcessor<OrderEventModel> eventProcessorF =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerF"), mySequenceBarrierF);
        MySequence consumeSequenceF = eventProcessorF.getCurrentConsumeSequence();
        // RingBuffer監聽消費者F的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceF);

        // ================================== 消費者G依賴上游的消費者D,通過消費者D的序列號建立序列屏障(構成消費的順序依賴)
        MySequenceBarrier mySequenceBarrierG = myRingBuffer.newBarrier(consumeSequenceD);
        // 基於序列屏障,建立消費者G
        MyBatchEventProcessor<OrderEventModel> eventProcessorG =
                new MyBatchEventProcessor<>(myRingBuffer, new OrderEventHandlerDemo("consumerG"), mySequenceBarrierG);
        MySequence consumeSequenceG = eventProcessorG.getCurrentConsumeSequence();
        // RingBuffer監聽消費者G的序列
        myRingBuffer.addGatingConsumerSequenceList(consumeSequenceG);

        // 啟動消費者執行緒
        new Thread(eventProcessorA).start();
        new Thread(eventProcessorB).start();
        new Thread(eventProcessorC).start();
        new Thread(eventProcessorD).start();
        new Thread(eventProcessorE).start();
        new Thread(eventProcessorF).start();
        new Thread(eventProcessorG).start();

        // 生產者釋出100個事件
        for(int i=0; i<100; i++) {
            long nextIndex = myRingBuffer.next();
            OrderEventModel orderEvent = myRingBuffer.get(nextIndex);
            orderEvent.setMessage("message-"+i);
            orderEvent.setPrice(i * 10);
            System.out.println("生產者釋出事件:" + orderEvent);
            myRingBuffer.publish(nextIndex);
        }

        // 簡單阻塞下,避免還未消費完主執行緒退出
        Thread.sleep(5000L);
    }
}
  • 最上游的的消費者在建立時傳入的序列屏障內只維護了生產者序列(myRingBuffer.newBarrier())。
  • 存在上游依賴的消費者在建立時,其傳入的序列屏障內需要維護直接依賴的上游消費者的序列號集合(即dependentSequencesList)用於控制消費速度不超過上游消費者。
  • 每個新建立的消費者都需要通過ringBuffer.addGatingConsumerSequenceList介面將自己的序列號維護到生產者的gatingConsumerSequenceList中,令生產者生產時不會越過最慢的消費者一圈。

總結

  • 比起v1版本的MyDisruptor,v2版本在做了較小的改動後就支援了多消費者消費者的組間消費依賴功能。
  • 通過小步快跑,逐步迭代的方式一點點的實現MyDisruptor並進行解析,希望可以讓讀者在一個更低複雜度的程式碼實現中更好的理解lmax-disruptor佇列中的精妙設計。

disruptor無論在整體設計還是最終程式碼實現上都有很多值得反覆琢磨和學習的細節,希望能幫助到對disruptor感興趣的小夥伴。

本篇部落格的完整程式碼在我的github上:https://github.com/1399852153/MyDisruptor 分支:feature/lab2

相關文章