單機最快的佇列Disruptor解析和使用

我有八千部下發表於2023-04-05

前言

介紹高效能佇列Disruptor原理以及使用例子。

Disruptor是什麼?

Disruptor是外匯和加密貨幣交易所運營商 LMAX group 建立高效能的金融交易所的結果。用於解決生產者、消費者及其資料儲存的設計問題的高效能佇列實現。可以對標JDK中的ArrayBlockingQueue。是目前單機且基於記憶體儲存的最高效能的佇列實現。見 與ArrayBlockingQueue效能對比

Disruptor高效能秘訣

使用CAS代替鎖

鎖非常昂貴,因為它們在競爭時需要仲裁。這種仲裁是透過到作業系統核心的上下文切換來實現的,該核心將掛起等待鎖的執行緒,直到它被釋放。系統提供的原子操作CAS(Compare And Swap/Set)是很好的鎖替代方案,Disruptor中同步就是使用的這種。

比如多生產者模式中com.lmax.disruptor.MultiProducerSequencer就是用了Java裡sun.misc.Unsafe類基於CAS實現的API。

image-20210922160128392

等待策略com.lmax.disruptor.BlockingWaitStrategy使用了基於CAS實現的ReentrantLock。

image-20210922160301925

獨佔快取行

為了提高效率CPU硬體不會以位元組或字為單位移動記憶體,而是以快取行,通常大小為 32-256 位元組的快取行,最常見的快取行是 64 位元組。這意味著,如果兩個變數在同一個快取行中,並且由不同的執行緒寫入,那麼它們會出現與單個變數相同的寫入爭用問題。為了獲得高效能,如果要最小化爭用,那麼確保獨立但同時寫入的變數不共享相同的快取行是很重要的。

比如com.lmax.disruptor.RingBuffer中屬性前後都用未賦值的long來獨佔。com.lmax.disruptor.SingleProducerSequencerPad也有相同處理方式。

image-20210922151808613

image-20210922151833921

環形佇列

  • 使用有界佇列,減少執行緒爭用

佇列相比連結串列在訪問速度上佔據優勢,而有界佇列相比可動態擴容的無界佇列則避免擴容產生的同步問題效率更高。Disruptor和JDK中的ArrayBlockingQueue一樣使用有界佇列。佇列長度要設為2的n次冪,有利於二進位制計算。

  • 使用環形陣列,避免生產和消費速度差異導致佇列頭和尾爭用

Disruptor在邏輯上將陣列的的頭尾看成是相連的,即一個環形陣列(RingBuffer)。

  • Sequence

生產和消費都需要維護自增序列值(Sequence),從0開始。

生產方只維護一個代表生產的最後一個元素的序號。代表生產的最後一個元素的序號。每次向Disruptor釋出一個元素都呼叫Sequenced.next()來獲取下個位置的寫入權。

在單生產者模式(SINGLE)由於不存在併發寫入,則不需要解決同步問題。在多生產者模式(MULTI)就需要藉助JDK中基於CAS(Compare And Swap/Set)實現的API來保證執行緒安全。

多個消費者各自維護自己的消費序列值(Sequence)儲存陣列中。

而環形透過與運算(sequence & indexMask)實現的,indexMask就是環形佇列的長度-1。以環形佇列長度8為例,第9個元素Sequence為8,8 & 7 = 0,剛好又回到了陣列第1個位置。

見com.lmax.disruptor.RingBuffer.elementAt(long sequence)

image-20210922184325086

預分配記憶體

環形佇列存放的是Event物件,而且是在Disruptor建立的時候呼叫EventFactory建立並一次將佇列填滿。Event儲存生產者生產的資料,消費也是透過Event獲取,後續生產則只需要替換掉Event中的屬性值。這種方式避免了重複建立物件,降低JVM的GC產頻率。

見com.lmax.disruptor.RingBuffer.fill(EventFactory eventFactory)

image-20210922184413900

消費者8種等待策略

當消費速度大於生產速度情況下,消費者執行的等待策略。

策略類名 描述
BlockingWaitStrategy(常用) 使用ReentrantLock,失敗則進入等待佇列等待喚醒重試。當吞吐量和低延遲不如CPU資源重要時使用。
YieldingWaitStrategy(常用) 嘗試100次,全失敗後呼叫Thread.yield()讓出CPU。該策略將使用100%的CPU,如果其他執行緒請求CPU資源,這種策略更容易讓出CPU資源。
SleepingWaitStrategy(常用) 嘗試200次 。前100次直接重試,後100次每次失敗後呼叫Thread.yield()讓出CPU,全失敗執行緒睡眠(預設100納秒 )。
BusySpinWaitStrategy 執行緒一直自旋等待,比較耗CPU。最好是將執行緒繫結到特定的CPU核心上使用。
LiteBlockingWaitStrategy 與BlockingWaitStrategy類似,區別在增加了原子變數signalNeeded,如果兩個執行緒同時分別訪問waitFor()和signalAllWhenBlocking(),可以減少ReentrantLock加鎖次數。
LiteTimeoutBlockingWaitStrategy 與LiteBlockingWaitStrategy類似,區別在於設定了阻塞時間,超過時間後拋異常。
TimeoutBlockingWaitStrategy 與BlockingWaitStrategy類似,區別在於設定了阻塞時間,超過時間後拋異常。
PhasedBackoffWaitStrategy 根據時間引數和傳入的等待策略來決定使用哪種等待策略。當吞吐量和低延遲不如CPU資源重要時,可以使用此策略。

消費者序列

所有消費者的消費序列(Sequence)都放在一個陣列中,見com.lmax.disruptor.AbstractSequencer,透過SEQUENCE_UPDATER來更新對應的序列值。

image-20210922182346493

呼叫更新的地方在com.lmax.disruptor.RingBuffer.addGatingSequences(Sequence... gatingSequences)。

消費太慢佇列滿了怎麼辦?

生產者執行緒被阻塞。生產者呼叫Sequenced.next()爭奪寫入權的時候需要判斷最小的消費序列值進行比較。如果寫入的位置還未消費則會進入迴圈不斷獲取最小消費序列值進行比較。

見包com.lmax.disruptor下SingleProducerSequencer或MultiProducerSequencer中next(int n)方法。

image-20210922183913542

Disruptor開發步驟

  • 建立Event、EventFactory、EventHandler和ExceptionHandler類

Event是環形佇列(RingBuffer)中的元素,是生產者資料的載體;EventFactory是定義Event建立方式的工廠類;EventHandler則是Event的處理器,定義如何消費Event中的資料。

另外有必要定義一個消費異常處理器ExceptionHandler,它是和EventHandler繫結的。當EventHandler.onEvent()執行丟擲異常時會執行對應的異常回撥方法。

  • 例項化Disruptor

建立Disruptor需要指定5個引數eventFactory、ringBufferSize、threadFactory、producerType、waitStrategy。

EventFactory是上面定義的Event工廠類;

ringBufferSize是環形佇列的長度,這個值要是2的N次方;

threadFactory是定義消費者執行緒建立方式的工廠類;

producerType是指明生產者是一個(SINGLE)還是多個(MULTI)。預設是MULTI,會使用CAS(Compare And Swap/Set)保證執行緒安全。如果指定為SINGLE,則不使用沒必要的CAS,使單執行緒處理更高效。

waitStrategy指明消費者等待生產時的策略。

  • 設定消費者

指明EventHandler並繫結ExceptionHandler。指定多個EventHandler時,會為每個EventHandler分配一個執行緒,一個Event會被多個並行EventHandler處理。

也可以指明多個WorkHandler,每個WorkHandler分配一個執行緒並行消費佇列中的Event,一個Event只會被一個WorkHandler處理。

  • 建立/例項化EventTranslator

EventTranslator定義生產者資料轉換為Event的方式,不同數量引數有不同的介面用來實現。

  • 最後用Disruptor.publishEvent() 來發布元素指明EventTranslator和引數

例子程式

  • 先引入Maven依賴
<dependency>
  <groupId>com.lmax</groupId>
  <artifactId>disruptor</artifactId>
  <version>3.4.4</version>
</dependency>
  • Event
/**
 * 事件
 *
 * @param <T>釋出的資料型別
 */
public class MyEvent<T> {

    private T data;

    public T getData() {
        return data;
    }

    public MyEvent<T> setData(T data) {
        this.data = data;
        return this;
    }
}
  • EventFactory
import com.lmax.disruptor.EventFactory;

/**
 * 建立事件的工廠
 *
 * @param <T>釋出的資料型別
 */
public class MyEventFactory<T> implements EventFactory<MyEvent<T>> {

    @Override
    public MyEvent<T> newInstance() {
        return new MyEvent<>();
    }
}
  • EventHandler
import com.lmax.disruptor.EventHandler;

/**
 * 事件消費方法
 *
 * @param <T>釋出的資料型別
 */
public class MyEventHandler<T> implements EventHandler<MyEvent<T>> {

    @Override
    public void onEvent(MyEvent<T> tMyEvent, long l, boolean b) throws Exception {
        System.out.println(Thread.currentThread().getName() + "MyEventHandler消費:" + tMyEvent.getData());
    }
}
  • ExceptionHandler
import com.lmax.disruptor.ExceptionHandler;

/**
 * 消費者異常處理器
 *
 * @param <T>釋出的資料型別
 */
public class MyExceptionHandler<T> implements ExceptionHandler<MyEvent<T>> {

    @Override
    public void handleEventException(Throwable ex, long sequence, MyEvent<T> event) {
        System.out.println("handleEventException");
    }

    @Override
    public void handleOnStartException(Throwable ex) {
        System.out.println("handleOnStartException");
    }

    @Override
    public void handleOnShutdownException(Throwable ex) {
        System.out.println("handleOnShutdownException");
    }
}

單消費者

import com.lmax.disruptor.EventTranslatorOneArg;
import com.lmax.disruptor.ExceptionHandler;
import com.lmax.disruptor.SleepingWaitStrategy;
import com.lmax.disruptor.WaitStrategy;
import com.lmax.disruptor.dsl.Disruptor;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import static com.lmax.disruptor.dsl.ProducerType.SINGLE;

/**
 * 單消費者
 */
public class SingleConsumerSample {

    public static void main(String[] args) {
        // 環形陣列長度,必須是2的n次冪
        int ringBufferSize = 1024;
        // 建立事件(Event)物件的工廠
        MyEventFactory<String> eventFactory = new MyEventFactory<>();
        // 建立消費者執行緒工廠
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 等待策略
        WaitStrategy waitStrategy = new SleepingWaitStrategy();
        Disruptor<MyEvent<String>> disruptor =
                new Disruptor<>(eventFactory, ringBufferSize, threadFactory, SINGLE, waitStrategy);

        // 指定一個處理器
        MyEventHandler<String> eventHandler = new MyEventHandler<>();
        disruptor.handleEventsWith(eventHandler);
        // 處理器異常處理器
        ExceptionHandler<MyEvent<String>> exceptionHandler = new MyExceptionHandler<>();
        disruptor.setDefaultExceptionHandler(exceptionHandler);

        disruptor.start();

        // 透過事件轉換器(EventTranslator)來指明如何將釋出的資料轉換到事件物件(Event)中
        // 這裡是一個引數的轉換器,另外還有兩個(EventTranslatorTwoArg)、三個(EventTranslatorThreeArg)
        // 和多個(EventTranslatorVararg)引數的轉換器可以使用,引數型別可以不一樣
        EventTranslatorOneArg<MyEvent<String>, String> eventTranslatorOneArg =
                new EventTranslatorOneArg<MyEvent<String>, String>() {
                    @Override
                    public void translateTo(MyEvent<String> event, long sequence, String arg0) {
                        event.setData(arg0);
                    }
                };

        // 釋出
        for (int i = 0; i < 10; i++) {
            disruptor.publishEvent(eventTranslatorOneArg, "One arg " + i);
        }

        disruptor.shutdown();
    }
}

單消費者Lambda寫法

這種只是迎合Java8 Lambda語法特性,程式碼更簡潔。

import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;

import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.stream.Collectors;

import static com.lmax.disruptor.dsl.ProducerType.SINGLE;

public class LambdaSample {


    public static void main(String[] args) {
        // 環形陣列長度,必須是2的n次冪
        int ringBufferSize = 1024;
        // 建立消費者執行緒工廠
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 等待策略
        WaitStrategy waitStrategy = new SleepingWaitStrategy();
        Disruptor<MyEvent<String>> disruptor =
                new Disruptor<>(MyEvent::new, ringBufferSize, threadFactory, SINGLE, waitStrategy);

        // 指定一個處理器
        EventHandler<MyEvent<String>> eventHandler = (event, sequence, endOfBatch) ->
                System.out.println(Thread.currentThread().getName() + "MyEventHandler消費:" + event.getData());
        disruptor.handleEventsWith(eventHandler);
        // 處理器異常處理器
        ExceptionHandler<MyEvent<String>> exceptionHandler = new MyExceptionHandler<>();
        disruptor.setDefaultExceptionHandler(exceptionHandler);

        disruptor.start();

        // 透過事件轉換器(EventTranslator)來指明如何將釋出的資料轉換到事件物件(Event)中
        // 一個引數的轉換器
        disruptor.publishEvent((event, sequence, param) -> event.setData(param), "One arg ");
        // 兩個引數的轉換器
        disruptor.publishEvent((event, sequence, pA, pB) -> event.setData(pA + pB), "Two arg ", 1);
        // 三個引數的轉換器
        disruptor.publishEvent((event, sequence, pA, pB, pC) -> event.setData(pA + pB + pC)
                , "Three arg ", 1, false);
        // 多個引數的轉換器
        disruptor.getRingBuffer().publishEvent((event, sequence, params) -> {
            List<String> paramList = Arrays.stream(params).map(Object::toString).collect(Collectors.toList());
            event.setData("Var arg " + String.join(",", paramList));
        }, "param1", "param2", "param3");

        disruptor.shutdown();
    }
}

多消費者重複消費元素

關鍵只在於指定多個EventHandler,並且EventHandler還可以分別繫結不同的ExceptionHandler。

每個EventHandler分配一個執行緒,一個Event會被每個EventHandler處理,適合兩個不同的業務都需要處理同一個元素的情況,類似廣播模式。

import com.lmax.disruptor.*;
import com.lmax.disruptor.dsl.Disruptor;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import static com.lmax.disruptor.dsl.ProducerType.SINGLE;

/**
 * 一個元素多個消費者重複消費
 */
public class RepetitionConsumerSample {

    public static void main(String[] args) {
        // 環形陣列長度,必須是2的n次冪
        int ringBufferSize = 1024;
        // 建立事件(Event)物件的工廠
        MyEventFactory<String> eventFactory = new MyEventFactory<>();
        // 建立消費者執行緒工廠
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 等待策略
        WaitStrategy waitStrategy = new SleepingWaitStrategy();
        Disruptor<MyEvent<String>> disruptor =
                new Disruptor<>(eventFactory, ringBufferSize, threadFactory, SINGLE, waitStrategy);


        // 這裡指定了2個消費者,那就會產生2個消費執行緒,一個事件會被消費2次
        EventHandler<MyEvent<String>> eventHandler = (event, sequence, endOfBatch) ->
                System.out.println(Thread.currentThread().getName() + "MyEventHandler消費:" + event.getData());
        EventHandler<MyEvent<String>> eventHandler2 = (event, sequence, endOfBatch) ->
                System.out.println(Thread.currentThread().getName() + "MyEventHandler——2消費:" + event.getData());
        disruptor.handleEventsWith(eventHandler, eventHandler2);
        // 分別指定異常處理器
        ExceptionHandler<MyEvent<String>> exceptionHandler = new MyExceptionHandler<>();
        disruptor.handleExceptionsFor(eventHandler).with(exceptionHandler);
        disruptor.handleExceptionsFor(eventHandler2).with(exceptionHandler);

        disruptor.start();

        for (int i = 0; i < 10; i++) {
            disruptor.publishEvent((event, sequence, param) -> event.setData(param), "One arg " + i);
        }

        disruptor.shutdown();
    }
}

多消費者

關鍵只在於定義WorkHandler,然後例項化多個來消費。

每個WorkHandler分配一個執行緒,一個元素只會被一個WorkHandler處理。

import com.lmax.disruptor.ExceptionHandler;
import com.lmax.disruptor.SleepingWaitStrategy;
import com.lmax.disruptor.WaitStrategy;
import com.lmax.disruptor.WorkHandler;
import com.lmax.disruptor.dsl.Disruptor;

import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;

import static com.lmax.disruptor.dsl.ProducerType.SINGLE;

public class MultiConsumerSample {

    public static void main(String[] args) {
        // 環形陣列長度,必須是2的n次冪
        int ringBufferSize = 1024;
        // 建立事件(Event)物件的工廠
        MyEventFactory<String> eventFactory = new MyEventFactory<>();
        // 建立消費者執行緒工廠
        ThreadFactory threadFactory = Executors.defaultThreadFactory();
        // 等待策略
        WaitStrategy waitStrategy = new SleepingWaitStrategy();
        Disruptor<MyEvent<String>> disruptor =
                new Disruptor<>(eventFactory, ringBufferSize, threadFactory, SINGLE, waitStrategy);

        // 處理器異常處理器
        ExceptionHandler<MyEvent<String>> exceptionHandler = new MyExceptionHandler<>();
        disruptor.setDefaultExceptionHandler(exceptionHandler);

        // 設定2個消費者,2個執行緒,一個Event只被一個消費者消費
        WorkHandler<MyEvent<String>> workHandler = tMyEvent ->
                System.out.println(Thread.currentThread().getName() + "WorkHandler消費:" + tMyEvent.getData());
        disruptor.handleEventsWithWorkerPool(workHandler, workHandler2);

        disruptor.start();

        for (int i = 0; i < 10; i++) {
            disruptor.publishEvent((event, sequence, param) -> event.setData(param), "One arg " + i);
        }

        disruptor.shutdown();
    }
}

參考連結

Disruptor 主頁

Disruptor 技術文件

GitHub Disruptor

GitHub Disruptor Getting Started

Maven Repository Disruptor Framework

LMAX 官網

相關文章