Disruptor入门

703 阅读7分钟

一、介绍

Disruptor是英国外汇交易公司LMAX在2010年之前开发的一个高性能队列,研发的初衷是解决内存队列的延迟问题。与Kafka、RabbitMQ用于服务间的消息队列不同,disruptor一般用于单机JVM线程间消息的传递。作为高性能消息队列开源框架,Disruptor是金融与游戏领域的常用开发组件之一,也是Java日志框架和流处理框架底层的常用依赖。 从功能上来看,Disruptor 实现了“队列”的功能,而且是一个有界队列(是一个可选无锁的线程间通信库)。作用与 ArrayBlockingQueue 有相似之处,可以实现生产者-消费者模式,但是 disruptor 从功能、性能上又都远好于 ArrayBlockingQueue。

二、队列

先进先出的一种数据结构,Java中存在一些内置队列。

2.1 ArrayBlockingQueue

阻塞队列,底层是数组,有边界限制,加锁保证线程安全,使用add()、offer() 数组满了直接报错(不阻塞);使用put()数组满了,进行notFull.await()操作,当出队和删除元素时唤醒 put() 操作(阻塞);使用take(),数组为空时,进行notEmpty.await(),当有元素入队时唤醒。 下面的集合类都有类似的阻塞和非阻塞的操作方法。

2.2 LinkedBlockingQueue

阻塞队列,底层是链表,无边界限制,加锁保证线程安全,需要考虑实际使用中的内存问题,防止溢出。

2.3 ConcurrentLinkedQueue

并发阻塞队列,底层是链表,无边界限制,CAS保证线程安全。

2.4 PriorityBlockingQueue

优先级队列,底层是堆,无边界限制,加锁保证线程安全,针对元素优先级有过多的处理,一般场景不适用。

性能考虑方面,CAS是优于加锁的,但是基于CAS实现的LinkedBlockingQueue是无边界的,实际开发场景中,需要考虑到生产者过快可能造出的内存溢出的问题,有边界的限制高于性能,因此ArrayBlockingQueue 通常是最适合的选择。但是这样『矮子里面选将军』,选出来的ArrayBlockingQueue和Disruptor进行对比,还是有很大的差距。

三、性能

3.1 Disruptor对比ArrayBlockingQueue

下面是官方给出的和ArrayBlockingQueue对比测试结果: P—生产者,C—消费者。 不同的组合形式

截屏2022-08-18 16.30.01.png

a.吞吐量测试数据对比(每秒) 截屏2022-08-18 16.40.03.png

Disruptor比ArrayBlockingQueue吞吐量高4~7倍。

b.按照Pipeline: 1P – 3C的连接模式测试延迟,生产者两次写入之间的延迟为1微秒,重复5000万次。延迟数据时间对比(纳秒)

截屏2022-08-18 16.41.13.png

Disruptor平均延迟为52纳秒,ArrayBlockingQueue平均延迟为32757纳秒,相差3个数量级。

3.2 Disruptor应用

Disruptor在 log4j,以及 activeMQ 源码扩展中都有使用。由于采用了 Disruptor,Log4j 2 性能明显优于 Log4j 1.x,Logback 和 java.util.logging,尤其是在多线程应用程序中,异步记录器的吞吐量比 Log4j 1.x 和 Logback 高 18 倍,延迟更低。

a.吞吐量

Untitled.png

loggers all async采用的是Disruptor,而Async Appender采用的是ArrayBlockingQueue队列。由图可见,单线程情况下,loggers all async与Async Appender吞吐量相差不大,但是在64个线程的时候,loggers all async的吞吐量比Async Appender增加了12倍,是Sync模式的68倍。

b.延迟

Untitled.png

明显看出loggers all async的延迟原优于Async Appender。

四、概念与核心类

Untitled.png

Disruptor的核心是一个环形无锁高速队列,这里的无锁是指不使用锁来保证原子性,而通过使用CAS保证原子性,提高了速度。并且底层实现为数组,数组长度必须为2的幂次方,与HashMap容量限制的理由相同,方便使用位运算来替代取模运算,更快的计算下标。

截屏2022-08-18 17.23.37.png

4.1 Ring Buffer

disruptor的数据结构,一个环形队列,主要负责放入生产的产品(事件),供消费者消费。

4.2 Sequence

序列号,通过顺序递增的序号来编号管理通过其进行交换的产品(事件),对产品(事件)的处理过程总是沿着序号逐个递增处理。一个 Sequence 用于跟踪标识某个特定的事件处理者( RingBuffer/Consumer )的处理进度。Sequence %数组长度 = Sequence对应的数组下标,生产者每生产一个产品,Sequence自增,因为是long类型,所以不用担心序列号消耗完。 由ProductBarrier和ConsumerBarrier分别确定生产者和消费者要操作的下标。

4.3 Event

生产者和消费者之间进行交换的数据被称为事件(Event),即生产者生产的产品。

4.4 EventFactory

创建事件的工厂,Disruptor 定义了接口,由用户实现,用来标识RingBuffer中存储的事件类型。

4.5 Producer

生产者,Disruptor 没有定义特定接口或类型,需要用户自己定义生产者的操作。

4.6 EventHandler

消费者,Disruptor 定义了接口,由用户实现,用于处理事件,是 Consumer 的真正实现。提供了EventHandlerWorkHandler两个接口,前者适用于单个消息被所有消费者消费(广播模式),后者适用于单个消息被多个消费者任意一个消费者消费(集群模式)。

4.7 ProductType

生产者类型:单生产者、多生产者。

4.8 Wait Strategy

等待策略,当队列里的数据都被消费完之后,消费者和生产者之间的等待策略。 消费者的WaitStrategy等待策略有8种实现类,可以分为有锁和无锁两大类,然后每一种都有其适用的场合,没有最好的WaitStrategy等待策略,只有适合自己应用场景的等待策略。

截屏2022-08-18 18.18.35.png

五、使用

依赖:

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

5.1 定义基础类

5.1.1 事件类

@Data
public class ProductEvent {
    private String uid;
}

5.1.2 事件工厂

public class ProductEventFactory implements EventFactory<ProductEvent> {
    @Override
    public ProductEvent newInstance() {
        return new ProductEvent();
    }
}

5.1.3 生产者

public class ProductEventProducer {
    private final Logger logger = LoggerFactory.getLogger(ProductEventProducer.class);
    private final RingBuffer<ProductEvent> ringBuffer;

    public ProductEventProducer(RingBuffer<ProductEvent> ringBuffer) {
        this.ringBuffer = ringBuffer;
    }

    public void onData(String uid) {
        // ringBuffer是个队列,其next方法返回的是最后一条记录之后的位置,即可用位置
        long sequence = ringBuffer.next();
        logger.info("开始生产,uid={}", uid);
        try {
            // sequence位置取出的事件是空事件
            ProductEvent event = ringBuffer.get(sequence);
            // 空事件添加业务信息
            event.setUid(uid);
        } finally {
            // 发布
            ringBuffer.publish(sequence);
        }
    }

5.1.4 消费者

public class ProductEventHandler implements EventHandler<ProductEvent>, WorkHandler<ProductEvent> {
    private Logger logger = LoggerFactory.getLogger(ProductEventHandler.class);

    @Override
    public void onEvent(ProductEvent event, long sequence, boolean endOfBatch) throws Exception {
        logger.info("广播模式,线程{}消费者开始处理event:{}!", Thread.currentThread().getName(), event.getUid());

        System.out.println("消费者"+Thread.currentThread().getName()+"打印uid:" + event.getUid());
        Thread.sleep(3000);

        logger.info("广播模式,线程{}消费者处理event完毕!", Thread.currentThread().getName());
    }

    @Override
    public void onEvent(ProductEvent productEvent) throws Exception {
        logger.info("集群模式,线程{}消费者开始处理event:{}!", Thread.currentThread().getName(), productEvent.getUid());

        System.out.println("消费者"+Thread.currentThread().getName()+"打印uid:" + productEvent.getUid());
        //保证生产快于消费
        Thread.sleep(3000);

        logger.info("集群模式,线程{}消费者处理event完毕!", Thread.currentThread().getName());
    }
}

5.2 使用方式

5.2.1 单生产者单消费者

public class Main {
    public static void main(String[] args) {
        ThreadFactory threadFactory = new CustomizableThreadFactory("customer-pool-");
        //单生产者模式,指定ProducerType.SINGLE,性能会快一点
        Disruptor<ProductEvent> disruptor = new Disruptor<>(new ProductEventFactory(), 4, threadFactory, ProducerType.SINGLE, new YieldingWaitStrategy());

        //单消费者模式
        disruptor.handleEventsWith(new ProductEventHandler());

        //启动
        disruptor.start();

        RingBuffer<ProductEvent> ringBuffer = disruptor.getRingBuffer();
        ProductEventProducer producer = new ProductEventProducer(ringBuffer);

        for (int i = 0; i < 6; i++) {
            producer.onData(i + "," + UUID.randomUUID());
        }

        //终止
        disruptor.shutdown();
    }

}

运行结果:

截屏2022-08-18 20.32.04.png

5.2.2 单生产者多消费者(广播模式)

public class Main {
    public static void main(String[] args) {
        ThreadFactory threadFactory = new CustomizableThreadFactory("customer-pool-");
        //单生产者模式,指定ProducerType.SINGLE,性能会快一点
        Disruptor<ProductEvent> disruptor = new Disruptor<>(new ProductEventFactory(), 4, threadFactory, ProducerType.SINGLE, new YieldingWaitStrategy());

        //多消费者广播模式
        EventHandler<ProductEvent>[] consumers = new ProductEventHandler[4];
        for (int i = 0; i < consumers.length; i++) {
            consumers[i] = new ProductEventHandler();
        }

        disruptor.handleEventsWith(consumers);

        //启动
        disruptor.start();

        RingBuffer<ProductEvent> ringBuffer = disruptor.getRingBuffer();
        ProductEventProducer producer = new ProductEventProducer(ringBuffer);

        for (int i = 0; i < 6; i++) {
            producer.onData(i + "," + UUID.randomUUID());
        }

        //终止
        disruptor.shutdown();
    }
}

运行结果:(只截取部分)

截屏2022-08-18 20.40.39.png

5.2.3 单生产者多消费者(集群模式)

public class Main {
    public static void main(String[] args) {
        //使用WorkPool必须保证Consumer对应一个线程,否则当 RingBuffer 满的时候,Producer 和 Consumer 都会阻塞。因此Disruptor废弃了使用Executor的构造方法
        //ExecutorService executorService = new ThreadPoolExecutor(2, 5, 1, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(5, true), new CustomizableThreadFactory("customer-pool-"));
        //推荐使用的是提供 ThreadFactory 形式的构造器,后续会根据事件处理器的个数来新增对应的线程
        ThreadFactory threadFactory = new CustomizableThreadFactory("customer-pool-");
        //单生产者模式,指定ProducerType.SINGLE,性能会快一点
        Disruptor<ProductEvent> disruptor = new Disruptor<>(new ProductEventFactory(), 4, threadFactory, ProducerType.SINGLE, new YieldingWaitStrategy());

        //多消费者集群模式
        WorkHandler<ProductEvent>[] consumers = new ProductEventHandler[4];
        for (int i = 0; i < consumers.length; i++) {
            consumers[i] = new ProductEventHandler();
        }
        disruptor.handleEventsWithWorkerPool(consumers);

        //启动
        disruptor.start();

        RingBuffer<ProductEvent> ringBuffer = disruptor.getRingBuffer();
        ProductEventProducer producer = new ProductEventProducer(ringBuffer);

        for (int i = 0; i < 6; i++) {
            producer.onData(i + "," + UUID.randomUUID());
        }

        //终止
        disruptor.shutdown();
    }
}

运行结果:

截屏2022-08-18 20.34.23.png

5.2.4 多生产者多消费者(集群模式)

public class Main {
    public static void main(String[] args) {
        ThreadFactory threadFactory = new CustomizableThreadFactory("customer-pool-");
        //多生产者模式,指定ProducerType.SINGLE,性能会快一点
        Disruptor<ProductEvent> disruptor = new Disruptor<>(new ProductEventFactory(), 4, threadFactory, ProducerType.MULT, new YieldingWaitStrategy());

        //多消费者集群模式
        WorkHandler<ProductEvent>[] consumers = new ProductEventHandler[4];
        for (int i = 0; i < consumers.length; i++) {
            consumers[i] = new ProductEventHandler();
        }
        disruptor.handleEventsWithWorkerPool(consumers);

        //启动
        disruptor.start();

        RingBuffer<ProductEvent> ringBuffer = disruptor.getRingBuffer();
        //两个生产者
        ProductEventProducer producer1 = new ProductEventProducer(ringBuffer);
        ProductEventProducer producer2 = new ProductEventProducer(ringBuffer);

        //每个生产者生成5个事件
        for (int i = 0; i < 5; i++) {
            producer1.onData((10 + i) + "," + UUID.randomUUID());
            producer2.onData((20 + i) + "," + UUID.randomUUID());
        }

        //终止
        disruptor.shutdown();
    }
}

运行结果:

截屏2022-08-18 20.47.21.png

5.2.5 多边形组合

使用handleEventsWith().then()可以将多个消费者拼接起来,组成多种消费者依赖关系。 下面给出一个例子,消费者one完成后处理消费者two和消费者three,都完成后处理消费者four。

截屏2022-08-18 22.41.34.png

新的消费者handler

@Data
@AllArgsConstructor
public class LegoHandler implements EventHandler<ProductEvent>, WorkHandler<ProductEvent> {
    private final Logger logger = LoggerFactory.getLogger(LegoHandler.class);
    private String name;

    @Override
    public void onEvent(ProductEvent event, long sequence, boolean endOfBatch) throws Exception {
        logger.info("广播模式,消费者{}处理event:{}!", name, event.getUid());
    }

    @Override
    public void onEvent(ProductEvent event) throws Exception {
        logger.info("集群模式,消费者{}处理event:{}!", name, event.getUid());
    }
}

单消费者生成三个事件。

public class Main {
    public static void main(String[] args) {
        ThreadFactory threadFactory = new CustomizableThreadFactory("customer-pool-");
        //单生产者模式,指定ProducerType.SINGLE,性能会快一点
        Disruptor<ProductEvent> disruptor = new Disruptor<>(new ProductEventFactory(), 4, threadFactory, ProducerType.SINGLE, new YieldingWaitStrategy());

        //多消费者集群模式
        //组装模式
        disruptor.handleEventsWith(new LegoHandler("one"))
                 .then(new LegoHandler("two"), new LegoHandler("three"))
                 .then(new LegoHandler("four"));

        //启动
        disruptor.start();

        RingBuffer<ProductEvent> ringBuffer = disruptor.getRingBuffer();
        ProductEventProducer producer = new ProductEventProducer(ringBuffer);

        for (int i = 0; i < 3; i++) {
            producer.onData(i + "," + UUID.randomUUID());
        }

        //终止
        disruptor.shutdown();
    }
}

运行结果:

截屏2022-08-18 22.42.05.png

可以看到two和three在one只有才执行,four只有two和three都执行完毕才执行。

可以配置handleEventsWithWorkerPool().then()做到一组handler完成后都进行另一handler操作的样式。不再赘述。

六、注意项

6.1 ringbuffersize

ringbuffersize必须为2的幂次方,否则启动报错。

6.2 start

start只能调用一次,再次调用报错。

截屏2022-08-18 21.05.02.png

ConsumerInfo.start()是启动消费者线程。

截屏2022-08-18 21.05.20.png

6.3 无法判断生产消费流程是否完成

disruptor.shutdown()方法会终止Disruptor,终止前会阻塞的判断a.判断是否有未消费的事件,b.判断是否有消费者未消费完,只有都确定为否了才会进行终止操作。

截屏2022-08-18 21.24.52.png

超时shutdown,timeout设置为-1,表示一直阻塞,直到关闭。

截屏2022-08-18 21.26.01.png

hasBacklog()判断上面a、b两个条件,halt()进行终止操作。

截屏2022-08-18 21.28.14.png hasBacklog()被private修饰,用户无法调用,且consumerRepository也被private修饰,因此无法通过现有方法判断生产消费流程是否完成。

截屏2022-08-18 21.33.15.png

halt()实际是将消费者线程都终止,shutdown()相当于是更优雅的终止方式。

6.4 消费未完成就退出

(定时任务)主线程退出,消费者就算没处理完毕也会退出。

使用单生产者多消费者(集群模式)的代码,在disruptor.shutdown();之前增加System.*exit*(0);,模拟(定时任务)主线程提前退出。

运行结果:

截屏2022-08-18 20.59.04.png

可以看到最后一个event5,还没处理完毕便结束了程序。

6.5 无法重启

上面提到shutdown()终止Disruptor实际是将消费者线程终止,而consumerRepository用户是拿不到的,想再次启动消费者线程只能调用start(),但是shutdown()以后Disruptor的状态并没有改变,started为true,再次调用start()会报错。

shutdown()后继续生产会出现一个现象,只能生产,无法消费。

类似问题,官方在github上给出的回答是『Disruptor被设计出来的目标,不是为了完成短时间就结束的简单流程』。因此,实际开发中使用Disruptor并不会像上文示例代码中那样,在流程后调用 shutdown(),而是遇到相关异常等情况才调用。

6.6 集群模式无法完美监控消费者线程池

截屏2022-08-18 21.42.30.png

截屏2022-08-18 21.42.43.png 可以看到使用Exector作为消费者线程池的构造方法被标记为废弃,建议使用ThreadFactory作为线程池,原因是上面提到的使用WorkPool必须保证Consumer对应一个线程,否则当 RingBuffer 满的时候,Producer 和 Consumer 都会阻塞。使用ThreadFactory形式的构造方法,后续会根据事件处理器的个数来新增对应的线程。

目前项目都使用SpringBoot配置的形式来初始化线程池,ThreadPoolTaskExecutor类可以转为ThreadFactory,但是这样创建完Disruptor后,无法实时获取线程池数据。而转成Executor创建Disruptor后,只能监控core_pool_sizelargest_pool_size等初始化数据,task_countcompleted_task_count不变,原因是6.7 消费者的线程不会终止,即使队列中没有待消费的事件,消费者线程也一直处于active状态,不会终止,此时线程池监控程序认为task没有完成,因此task_countcompleted_task_count数值固定。

6.7 消费者线程不会终止

队列中没有待消费的事件,消费者线程也一直处于active状态,不会终止。

七、总结

Disruptor 的封装很薄(比起 Netty、Spring 之类的重量级框架),调用链路都相对较短,源码代码量也较少。

Disruptor Github wiki有关于Disruptor相关概念和原理的介绍,但是该wiki已经很久没有更新,观察提交历史也可以看出Disruptor并不活跃了,一方面是因为作为一个单机队列,在分布式的时代自然没落,更多的是因为它的成熟与完善。

本文只涉及到入门使用层面,更高级的如Sequence处理伪共享、底层设计框架与实现、Disruptor调优,并没有包含。

八、参考