Disruptor入门及应用

发布时间 2023-04-03 23:15:35作者: loveletters

Disruptor是什么

Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,能够在无锁的情况下实现网络的Queue并发操作,基于Disruptor开发的系统单线程能支撑每秒600万订单。

环境准备

  • JDK1.8
  • maven

我们先创建一个maven项目,并且引入disruptor相关的依赖和lombok


<dependency>
            <groupId>com.lmax</groupId>
            <artifactId>disruptor</artifactId>
            <version>3.4.2</version>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>

快速开始

我们可以通过以下步骤快速开始体验一下disruptor

  • 建立一个工厂Event类,用于创建Event类实例对象
  • 需要一个监听事件类,用于处理数据(Event类)
  • 编写生产者组件,向Disruptor容器中去投递数据
  • 实例化Disruptor实例,配置一系列参数,编写Disruptor核心组件

Event类

OrderEvent

package com.billetsdoux.disruptor_demo.quickstart;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class OrderEvent {
    // 订单的价格
    private long value;

}


工厂Event类

OrderEventFactory 需要实现EventFactory接口

package com.billetsdoux.disruptor_demo.quickstart;

import com.lmax.disruptor.EventFactory;

/**
 * 建立一个工厂Event类,用于创建Event类实例对象
 */
public class OrderEventFactory  implements EventFactory<OrderEvent> {

    // 这个方法就是为了返回空的数据对象
    @Override
    public OrderEvent newInstance() {
        return new OrderEvent();
    }
}

监听事件类

OrderEventHandler 需要实现EventHandler接口。我们这里简单的打印一下接受到的数据。

package com.billetsdoux.disruptor_demo.quickstart;

import com.lmax.disruptor.EventHandler;

public class OrderEventHandler  implements EventHandler<OrderEvent> {

    @Override
    public void onEvent(OrderEvent event, long sequence, boolean endOfBatch) throws Exception {
        System.out.println("消费者:" + event.getValue());
    }
}

编写生产者组件

OrderEventProducer 中需要持有一个RingBuffer对象,需要在我们实例化的时候传入这个对象。

我们在发送数据的时候分为以下几个步骤

  1. 在生产者发送消息的时候,首先需要从我们的ringBuffer里面获取一个可用的序号
  2. 根据这个序号找到具体的"OrderEvent" 元素,此时获取的对象没有被赋值
  3. 进行实际的赋值处理
  4. 提交发布操作

package com.billetsdoux.disruptor_demo.quickstart;

import com.lmax.disruptor.RingBuffer;
import lombok.AllArgsConstructor;

import java.nio.ByteBuffer;

@AllArgsConstructor
public class OrderEventProducer {

    private RingBuffer<OrderEvent> ringBuffer;

    public void sendData(ByteBuffer data){
        // 1.在生产者发送消息的时候,首先需要从我们的ringBuffer里面获取一个可用的序号

        long sequence = ringBuffer.next();

        try {
            // 2.根据这个序号找到具体的"OrderEvent" 元素,此时获取的对象没有被赋值

            OrderEvent event = ringBuffer.get(sequence);

            // 3.进行实际的赋值处理
            event.setValue(data.getLong(0));

        }finally {

            // 4.提交发布操作

            ringBuffer.publish(sequence);
        }

    }
}

实例化Disruptor实例

我们直接在main方法中实例化它并且启动。分为如下几个步骤

  1. 实例化Disruptor对象
  2. 添加消费者监听
  3. 启动Disruptor
  4. 发送数据

package com.billetsdoux.disruptor_demo.quickstart;

import com.lmax.disruptor.BlockingWaitStrategy;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;

import java.nio.ByteBuffer;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) {

        // 1.实例化disruptor对象

        OrderEventFactory orderEventFactory = new OrderEventFactory();

        int ringBufferSize = 1024*1024;

        ExecutorService  executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors());

        Disruptor<OrderEvent> disruptor = new Disruptor<OrderEvent>(
                orderEventFactory, // 消息(event)工厂对象
                ringBufferSize,    //  容器的长度
                executor,          // 线程池(建议使用自定义线程池)
                ProducerType.SINGLE, //单生产者模式
                new BlockingWaitStrategy() //等待策略,这里用的阻塞的策略
        );

        // 2.添加消费者监听(构建disruptor与消费者的关系)

        disruptor.handleEventsWith(new OrderEventHandler());

        // 3.启动disruptor

        disruptor.start();

        // 4. 获取实际存储数据的容器: RingBuffer

        RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();

        OrderEventProducer producer = new OrderEventProducer(ringBuffer);

        ByteBuffer bb = ByteBuffer.allocate(8);

        for (int i = 0; i < 100; i++) {
            bb.putLong(0,i);

            producer.sendData(bb);
        }

        disruptor.shutdown();

        executor.shutdown();


    }
}

点击运行,到此我们就已经简单的体验了Disruptor,我将在后续章节详细的介绍它的各个组件。

Disruptor核心原理

RingBuffer

它是一个环(首尾相接的环),你可以把它用做在不同上下文(线程)间传递数据的buffer。RingBuffer拥有一个序号,这个序号指向数组中下一个可用的元素。

随着你不停的填充这个buffer(或者是读取),这个序号会一直增长,直到绕过这个环。

要找到数组中当前序号指向的元素,可以通过mod操作

12(当前序号)% 10(数组长度)

Sequence

通过顺序递增的序号来编号,管理进行交换的数据(事件),对数据(事件)的处理过程总是沿着序号逐个递增处理。一个Sequence用于跟踪标识某个特定的事件处理者(RingBuffer/Producer/Consumer)的处理进度,Sequence可以看成是一个AtomicLong用于标识进度,还有另一个目的就是防止不同的Sequence之间的CPU缓存的伪共享问题。

Sequencer

Sequencer是Disruptor的真正核心

此接口有两个实现类

  • SingleProducerSequencer
  • MultiProducerSequencer

主要实现生产者和消费者之间快速、正确的传递数据的并发算法。

Sequence Barrier

用于保持对RingBuffer的Producer和Consumer之间的平衡关系;Sequence Barrier还定义来决定Consumer是否还有可处理的事件的逻辑。

WaitStrategy

决定一个消费者将如何等待生产者将Event置入Disruptor中。主要策略有:

  • BlockingWaitStrategy
  • SleepingWaitStrategy
  • YieldingWaitStrategy

BlockingWaitStrategy 是最低效的策略,但其对CPU的消耗最小并且在各种不同的部署环境中能够提供更加一致的性能表现。
SleepingWaitStrategy 的性能表现跟 BlockingWaitStrategy 差不多,对CPU的消耗也类似,但其对生产者线程影响最小,适合用于异步日志收集的场景。
YieldingWaitStrategy 的性能最好,适合用于低延迟系统。在要求极高性能且事件处理线程数小于CPU逻辑核心数的场景中,推荐使用此策略。

Event

从生产者到消费者过程中所处理的数据单元,Disruptor中没有代码表示Event,它是由用户自定义的。

EventProcessor

主要事件循环,处理Disruptor中的Event,拥有消费者的Sequence。它有一个实现类是BatchEventProcessor,包含event loop有效的实现,并且将回调到一个EventHandler接口的实现对象。

WorkProcessor

确保每个sequence只被一个processor消费者消费,在同一个WorkPool中处理多个WorkProcessor不会消费同样的sequence

进阶用法

我们在上一章节简单来介绍来Disruptor中各个主要组件的作用,我们将在本章节通过一些案例来进一步学习Disruptor的一些进阶用法。

串行和并行操作

Disruptor支持多消费者的,我们可以通过链式调用或者单独调用的方式添加消费者来实现串行跟并行操作。

EventHandlerGroup<T> handleEventsWith(final EventHandler<? super T>... handlers)

我们来继续回顾一下Disruptor的使用方式。
首先我们需要一个定义一个Event用于传递消息

package com.billetsdoux.disruptor_demo.advanced;

import lombok.Data;


import java.util.concurrent.atomic.AtomicInteger;

/**
 *  Event对象
 */
@Data
public class Trade {
    private String id;
    private String name;
    private double price;
    private AtomicInteger count = new AtomicInteger(0);

}

第二步本来应该定义一个EventFactory用于创建Event对象,这里我们直接用lambda的方式,就不去创建这个类了。

第三步创建生产者,我们这里模拟线程池来提交消息。我们首先需要实现Runnable接口,在之前的快速开始中我们使用RingBuffer来发布消息,我们这里也可以直接用Disruptor对象来发布消息,只是通过Disruptor发布消息需要一个实现来EventTranslator接口的对象。这种方式相比之前通过RingBuffer的方式要简单一点我们不需要拿到Sequence。

package com.billetsdoux.disruptor_demo.advanced;

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

import java.util.Random;
import java.util.concurrent.CountDownLatch;

public class TradePublisher implements Runnable {
    private Disruptor<Trade> disruptor;
    private CountDownLatch latch;
    private static int PUBLIC_COUNT = 1;
    public TradePublisher(CountDownLatch latch, Disruptor<Trade> disruptor) {
        this.latch = latch;
        this.disruptor = disruptor;
    }

    @Override
    public void run() {
        for (int i = 0; i < PUBLIC_COUNT; i++) {
            // 新的提交任务方式
            disruptor.publishEvent(new TradeEventTranslator());
        }
        latch.countDown();

    }
    class TradeEventTranslator implements EventTranslator<Trade>{
        private Random random = new Random();

        @Override
        public void translateTo(Trade event, long sequence) {
            this.generateTrade(event);

        }

        private void generateTrade(Trade event) {
            event.setPrice(random.nextDouble()*9999);
        }
    }
}

第四步创建消费者对象,我们这里要演示多消费者串并行操作,所以我们需要创建多个消费者。

消费者对象除了可以通过实现EventHandler接口外,还可以通过实现WorkHandler的方式来实现。

最后一步实例化Disruptor,我们这里2个线程池一个用来构建Disruptor对象,另外一个用来发送消息。

package com.billetsdoux.disruptor_demo.advanced;

import com.lmax.disruptor.BusySpinWaitStrategy;
import com.lmax.disruptor.EventFactory;
import com.lmax.disruptor.RingBuffer;
import com.lmax.disruptor.dsl.Disruptor;
import com.lmax.disruptor.dsl.ProducerType;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class Main {
    public static void main(String[] args) throws Exception {
        // 构建用于提交的线程池
        ExecutorService es = Executors.newFixedThreadPool(4);
        ExecutorService es2 = Executors.newFixedThreadPool(4);
        // 构建Disruptor
        Disruptor<Trade> disruptor = new Disruptor<Trade>(
                Trade::new,
                1024*1024,
                es2,
                ProducerType.SINGLE,
                new BusySpinWaitStrategy()

        );

        // 2 把消费者设置到Disruptor中handleEventsWith

        // 2.1 串行操作
        disruptor.handleEventsWith(new Handler1())
                .handleEventsWith(new Handler2())
                .handleEventsWith(new Handler3());


        // 3.启动Disruptor
        final RingBuffer<Trade> ringBuffer = disruptor.start();
        CountDownLatch latch = new CountDownLatch(1);
        long start = System.currentTimeMillis();
        es.submit(new TradePublisher(latch,disruptor));

        latch.await();

        disruptor.shutdown();
        es.shutdown();
        es2.shutdown();
        long end = System.currentTimeMillis();

        System.out.println("总耗时:"+(end-start));

    }



}

如果我们在disruptor.handleEventsWith()链式调用这个方法即为串行操作。我们运行一下查看耗时

可以看到依次打印来handler1、handler2、handler3中的输入并且耗时3秒多。

如果我们在disruptor.handleEventsWith()独立调用这个方法即为并行操作。我们再运行看一下结果。

可以看到在handler3中我们并没拿到handler1、2中设置的数据。并且耗时也只有2秒

菱形操作

Disruptor可以实现串并行同时编码。

在上一个例子中我们Handler1跟Handler2是可以并行操作的,但是Handler3应该等待前两个完成后再执行才能正确的获取到结果。我们可以通过这样的方式来进行

或者是

我们运行一下查看结果

六边形操作

我们新增2个Handler

Disruptor为我们提供来非常语义化的方法让我们来方便实现此类操作,我们来看一下如何编写代码。

我们运行一下查看结果,发现结果并不是如我们预期的那样。

直接说原因,是因为我们这里有5个Handler而且我们Disruptor是单消费者模式,这样每一个BatchEventProcessor都会占用一个线程,而我们这里的线程池只有4个线程,所以无法正常运行。我们只需要将线程池的线程数改成5个就能正常运行了,但是我们不可能在生成中每有一个消费者就增加一个线程数,所以我将在后续章节介绍它的多消费者模式来解决这个问题。

多消费竞争消费(不重复消费)

在之前我们做了对于一个消息多个消费者同时消费,我们接下来讲一下不重复消费。

这里就需要讲到handleEventsWithhandleEventsWithWorkerPool方法的区别

常用的方法是:disruptor.handleEventsWith(EventHandler ... handlers),将多个EventHandler的实现类传入方法,封装成一个EventHandlerGroup,实现多消费者消费。
disruptor的另一个方法是:disruptor.handleEventsWithWorkerPool(WorkHandler ... handlers),将多个WorkHandler的实现类传入方法,封装成一个EventHandlerGroup实现多消费者消费。两者共同点都是,将多个消费者封装到一起,供框架消费消息。

不同点在于,对于某一条消息m,handleEventsWith方法返回的EventHandlerGroup,Group中的每个消费者都会对m进行消费,各个消费者之间不存在竞争。handleEventsWithWorkerPool方法返回的EventHandlerGroup,Group的消费者对于同一条消息m不重复消费;也就是,如果c0消费了消息m,则c1不再消费消息m。传入的形参不同。对于独立消费的消费者,应当实现EventHandler接口。对于不重复消费的消费者,应当实现WorkHandler接口。

实例如下:

构建消费者对象,这里的消费者需要实现WorkHandler接口跟之前的有些区别

package com.billetsdoux.disruptor_demo.multi;

import com.lmax.disruptor.WorkHandler;

import java.util.Random;
import java.util.concurrent.atomic.AtomicInteger;

public class Consumer  implements WorkHandler<Order> {
    private  String consumerId;


    private  AtomicInteger count = new AtomicInteger(0);

    private Random random = new Random();

    public Consumer(String consumerId) {
        this.consumerId = consumerId;
    }

    @Override
    public void onEvent(Order event) throws Exception {
        //模拟消费耗时
        Thread.sleep(random.nextInt(5));

        System.out.println("当前消费:"+consumerId+",消费信息ID:"+event.getId());
        count.incrementAndGet();
        Main.totalCountDownLatch.countDown();
    }
    public int getCount(){
        return count.get();
    }
}


创建生产者

public class Producer {
    private RingBuffer<Order> ringBuffer;
    public Producer(RingBuffer<Order> ringBuffer) {
        this.ringBuffer = ringBuffer;
    }

    public void sendData(String uuid) {
        final long sequence = ringBuffer.next();

        try {
            final Order order = ringBuffer.get(sequence);
            order.setId(uuid);
        }finally {
            ringBuffer.publish(sequence);
        }
    }
}

实例化Disruptor,我们这里构建了100个生产者每个生产者发送100条数据,10个消费者去消费这些数据。注意这里我们需要用到handleEventsWithWorkerPool其他的跟之前的类似。

查看运行结果:

后记

此致我们已经简单的学习了Disruptor的基础知识,以及部分场景的使用方式,后续我们将通过一些案例来看一下Disruptor如何在实际项目中进行应用。