【战、面试官】java队列不行了?换成Disruptor吧!

2,375 阅读12分钟

我是「 kangarooking(袋鼠帝) 」 真心给大家分享经验和技术干货,面试次数100+。面试经验绝对丰富,也当过面试官,内容绝对用心可靠。点赞在看,养成习惯。关注me,每天进步亿点点 ❗ ❗ ❗

2022-6-23

前言

本章主要讲Disruptor的一些优秀的设计思想,我们主要学习它的设计思想和实现思路。本文对Disruptor的学习起到一个抛砖引玉的作用,也集合了个人学习过程中查阅的一些资料。通过对本章以及相关资料的学习(当然最主要的还是要自己动手去实践,debug体会),相信足以对Disruptor这个高性能队列有一个全面且深入的认识。

简单介绍一下概念

Disruptor是英国外汇交易公司LMAX开发的一个高性能队列,研发的初衷是解决内部的内存队列的延迟问题,而不是分布式队列。基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注。简单来说就是java的高性能内存队列,即在同一个JVM进程中的多线程间消息传递,引入依赖就能使用(这是目前的最新版本)。

    <!-- 高性能内存队列 -->
    <dependency>
    	<groupId>com.lmax</groupId>
    	<artifactId>disruptor</artifactId>
    	<version>3.4.4</version>
    </dependency>

说一下优势

据目前资料显示:应用Disruptor的知名项目有如下的一些:Storm,Camel,Log4j2,shenyu还有目前的美团点评技术团队也有很多不少的应用,或者说有一些借鉴了它的设计机制。

  • 使用广泛
  • 高性能
  • 无锁机制
  • 轻量
  • 支持多种生产消费模式

为啥子高性能

  • 通过填充cpu缓存行,解决cpu伪共享问题;
  • 预分配内存,在初始化的时候将环形数组内填充满对象。使得数组对象一直存在(除非程序终止)。这就意味着不需要花大量的时间用于垃圾回收;
  • 使用环形队列(环形数组),在一块连续的内存空间,利用缓存行的一次性加载64k的内存,提高了cpu缓存命中率;
  • 使用cas,无锁保证原子性;
  • 使用环形数组,只新增和覆盖数据。数组长度2^n, 通过位运算, 加快定位的速度, 下标采取递增的形式. 不用担心index溢出的问题。 index是long类型, 即使100万QPS的处理速度, 也需要30万年才能用完;
  • Disruptor3.x中,序号栅栏SequenceBarrier和序号Sequence搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用。

Disruptor3.x中,各个消费者和生产者持有自己的序号,这些序号的变化必须满足如下基本条件:

  • 消费者序号数值必须小于生产者序号数值
  • 消费者序号数值必须小于其前置(依赖关系)消费者的序号数值
  • 生产者序号数值不能大于消费者中最小的序号数值,以避免生产者速度过快,将还没来得及消费的消息覆盖

相关类以及属性介绍

有助于看源码时理解主要的一些属性的作用(这块可以阅读源码的时候在看)。

功能
AbstractSequencer记录生产者当前的进度,是生产者的游标
WorkPool共同消费者池,类似线程池管理
WorkProcessor共同消费者,每个共同消费者共同消费整个数据,解决不重复消费者场景
BatchEventProcessor独立消费者,每个独立消费者独自消费整个数据
ProcessingSequenceBarrier消费者的等待策略
MultiProducerSequencer用于统计当前环形数组中哪些位置是被写入的
所属类属性功能
AbstractSequencercursor记录生产者当前的进度,是生产者的游标
AbstractSequencerSequence[] gatingSequences存放WorkPool.workSequence以及各WorkProcessor.sequence的对象引用
WorkPoolworkSequence记录消费者当前进度
WorkProcessorworkSequence获取消费者当前进度(序号),或将要消费的序号,和WorkPool.workSequence是同一个
WorkProcessorsequence存放消费者已经消费到的最新序号,WorkPool.sequence使用的同一个实例
WorkProcessorsequenceBarrier每个WorkProcessor(共同消费者)的SequenceBarrier实例是同一个
BatchEventProcessorsequenceBarrierBatchEventProcessor(独立消费者)的SequenceBarrier实例是同一个
BatchEventProcessorsequenceBatchEventProcessor(独立消费者)的sequence实例是独有的
ProcessingSequenceBarrierwaitStrategy消费者的等待策略
ProcessingSequenceBarrierdependentSequence如果消费者不依赖于其它的消费者,值同cursorSequence
ProcessingSequenceBarriercursorSequence和上面的cursor是同一个引用
ProcessingSequenceBarriersequencer等于上面的AbstractSequencer
MultiProducerSequencerint[] availableBuffer用于统计当前环形数组中哪些位置是被写入的
MultiProducerSequencergatingSequenceCache存放消费者最近消费的最小序号,在发现追尾的时候去更新它,而不是直接去消费者那边获取,减少因为消费者序号经常改变缓存行失效导致的伪共享问题
AbstractSequencerSequence[] gatingSequences存放所有消费者的sequence,对于共同消费者,它还存放了WorkPool的workSequence

WorkProcessor(共同消费者):多个消费者消费的时候不重复消费;

BatchEventProcessor(独立消费者):多个消费者消费的时候每个消费者都消费全量数据。

DEMO代码

定义消息类

/**
 * 消息
 */
@Data
public class OrderEvent {
    private int order;
    private Long orderId;
}

定义消息工厂类,实现disruptor内部的接口EventFactory

/**
 * 消息工厂
 */
public class OrderEventFactory implements EventFactory<OrderEvent> {
    
    /**
     * 在disruptor初始化过程中,预分配内存时被调用
     * @return
     */
    @Override
    public OrderEvent newInstance() {
        return new OrderEvent();
    }
}

定义消费者,实现EventHandler接口,也可以实现WorkHandler接口。区别是EventHandler实现的每个消费者都会消费全量数据,而WorkHandler实现的所有消费者共同来消费全量数据。

/**
 * 消费者
 */
public class OrderEventHandler implements EventHandler<OrderEvent> {
    @Override
    public void onEvent(OrderEvent orderEvent, long sequence, boolean endOfBatch) throws Exception {
        System.out.println("orderEvent=" + orderEvent + "  sequence=" + sequence + "  endOfBatch=" + endOfBatch);
    }
}

初始化并启动disruptor,注入消费者们,同时开启一个生产者。

public class OrderEventMain {
    public static void main(String[] args) throws Exception {
        int bufferSize = 16;
        //初始化disruptor队列
        Disruptor<OrderEvent> disruptor = new Disruptor<>(new OrderEventFactory(), bufferSize, DaemonThreadFactory.INSTANCE);
        //初始化两种消费者
        OrderEventHandler orderEventHandler1 = new OrderEventHandler();
        //初始化两个独立消费者
        disruptor.handleEventsWith(orderEventHandler1, (e, l, end) -> {
            System.out.println("order=" + e.getOrder());
            Thread.sleep(500);
        });
        //初始化三个共同消费者
        EventHandlerGroup<OrderEvent> orderEventEventHandlerGroup = disruptor.handleEventsWithWorkerPool((orderEvent) -> {
                    System.out.println("不可重复消费1 消息=" + orderEvent.getOrder());
                }, (orderEvent) -> {
                    System.out.println("不可重复消费2 消息=" + orderEvent.getOrder());
                },
                (orderEvent) -> {
                    System.out.println("不可重复消费3 消息=" + orderEvent.getOrder());
                });
//                .thenHandleEventsWithWorkerPool(new OrderEventWorkHandler());
        //下面是生产者,往disruptor里面丢数据
        RingBuffer<OrderEvent> ringBuffer = disruptor.getRingBuffer();
        //开启所有消费者监听数据
        disruptor.start();

        ByteBuffer bb = ByteBuffer.allocate(8);
        for (int i = 0; i < 1000; i++) {
            bb.putLong(0, i);
            int finalI = i;
            ringBuffer.publishEvent((event, sequence, buffer) -> {
                event.setOrderId(buffer.getLong(0));
                event.setOrder(finalI);
            }, bb);
            Thread.sleep(300);
        }
    }
}

庖丁解牛

初始化disruptor队列

OrderEventMainnew Disruptor<>(new OrderEventFactory(), bufferSize, DaemonThreadFactory.INSTANCE);debug下来找到这个方法。

    public static <E> RingBuffer<E> createMultiProducer(
        EventFactory<E> factory,
        int bufferSize,
        WaitStrategy waitStrategy)
    {
        MultiProducerSequencer sequencer = new MultiProducerSequencer(bufferSize, waitStrategy);

        return new RingBuffer<E>(factory, sequencer);
    }
    public MultiProducerSequencer(int bufferSize, final WaitStrategy waitStrategy)
    {
        super(bufferSize, waitStrategy);
        availableBuffer = new int[bufferSize];
        indexMask = bufferSize - 1;
        indexShift = Util.log2(bufferSize);
        initialiseAvailableBuffer();
    }

availableBuffer进行初始化,用于后面消费者判断当前消费进度是否超过生产进度,如果超过就执行相应的等待策略。

    private void initialiseAvailableBuffer()
    {
        for (int i = availableBuffer.length - 1; i != 0; i--)
        {
            setAvailableBufferValue(i, -1);
        }

        setAvailableBufferValue(0, -1);
    }

初始化时都是-1,当生产者生产第一圈数据时,会逐序将0-16下标的-1更新为0,生产第二圈数据时逐序将0-16下标的0更新为1,这样消费者就能感知生产者生产的进度了。

1.png

创建RingBuffer

    RingBufferFields(
        EventFactory<E> eventFactory,
        Sequencer sequencer)
    {
        this.sequencer = sequencer;
        this.bufferSize = sequencer.getBufferSize();

        if (bufferSize < 1)
        {
            throw new IllegalArgumentException("bufferSize must not be less than 1");
        }
        if (Integer.bitCount(bufferSize) != 1)
        {
            throw new IllegalArgumentException("bufferSize must be a power of 2");
        }

        this.indexMask = bufferSize - 1;
        this.entries = new Object[sequencer.getBufferSize() + 2 * BUFFER_PAD];
        fill(eventFactory);
    }

通过调用EventFactory实现类的newInstance方法来填充数组,从而达到预分配内存的作用。这里也是给开发者留的一个扩展点,可以在newInstance方法中自定义预分配对象的样子。

2.png

通过填充缓存行,来解决伪共享问题。

3.png

不理解为什么它左右都填充数据的同学参考:www.jianshu.com/p/e1a1b950f…

注入及启动消费者(以共同消费者为列)

handleEventsWithWorkerPool其实就是把这个消费者对象放入disruptor内部的集合中。

直接看start,最终会调用到这个方法,也就是循环从消费者集合中获取消费者,然后执行,消费者查看调用关系实际上是Runable的子类。

4.png

我们以WorkProcessor为例,查看它的run方法

    public void run()
    {
        if (!running.compareAndSet(false, true))
        {
            throw new IllegalStateException("Thread is already running");
        }
        sequenceBarrier.clearAlert();
        //这里类似spring的Aware,是留给开发者的扩展点,可以在消费的开始做一些自定义操作
        notifyStart();
        //已处理序列(流程控制,判断当前序列的消息是否已被处理)
        boolean processedSequence = true;
        //缓存的可用序列
        long cachedAvailableSequence = Long.MIN_VALUE;
        //下一个序列
        long nextSequence = sequence.get();
        T event = null;
        //循环从队列中获取数据
        while (true)
        {
            try
            {
                // if previous sequence was processed - fetch the next sequence and set
                // that we have successfully processed the previous sequence
                // typically, this will be true
                // this prevents the sequence getting too far forward if an exception
                // is thrown from the WorkHandler
                if (processedSequence)
                {
                    processedSequence = false;
                    do
                    {
                        //获取消费者当前的进度并+1,得到本次要消费者的序号
                        //nextSequence是将要消费,但是还未实际消费的序号
                        nextSequence = workSequence.get() + 1L;
                        //sequence里面存放消费者当前的进度
                        sequence.set(nextSequence - 1L);
                    }
                    //cas判断当前消费者是否可以获得nextSequence的消费权
                    while (!workSequence.compareAndSet(nextSequence - 1L, nextSequence));
                }
                //cachedAvailableSequence代表申请到的最大消费序号
                //nextSequence要小于这个序号才能执行消费逻辑
                if (cachedAvailableSequence >= nextSequence)
                {
                    event = ringBuffer.get(nextSequence);
                    workHandler.onEvent(event);
                    processedSequence = true;
                }
                else
                {
                    //否则就要等待生产者跟上生产进度
                    //并重新获取一段空间进行消费(这段空间就是nextSequence到cachedAvailableSequence之间的序号范围)
                    cachedAvailableSequence = sequenceBarrier.waitFor(nextSequence);
                }
            }
            catch (final TimeoutException e)
            {
                notifyTimeout(sequence.get());
            }
            catch (final AlertException ex)
            {
                if (!running.get())
                {
                    break;
                }
            }
            catch (final Throwable ex)
            {
                // handle, mark as processed, unless the exception handler threw an exception
                exceptionHandler.handleEventException(ex, nextSequence, event);
                processedSequence = true;
            }
        }

        notifyShutdown();

        running.set(false);
    }

五种WaitStrategy(等待策略):

  • BlockingWaitStrategy:默认策略,没有获取到任务的情况下线程会进入等待状态。cpu 消耗少,但是延迟高。

  • TimeoutBlockingWaitStrategy:相对于BlockingWaitStrategy来说,设置了等待时间,超过后抛异常。

  • BusySpinWaitStrategy:线程一直自旋等待。cpu 占用高,延迟低.

  • YieldingWaitStrategy:尝试自旋 100 次,然后调用 Thread.yield() 让出 cpucpu 占用高,延迟低。

  • SleepingWaitStrategy:尝试自旋 100 此,然后调用 Thread.yield() 100 次,如果经过这两百次的操作还未获取到任务,就会尝试阶段性挂起自身线程。此种方式是对cpu 占用和延迟的一种平衡,性能不太稳定。

生产者

生产数据就两步:

1.获取当前能生产数据的序号;

2.往这个序号生产数据,其实就是为已经实例化的对象赋值。

生产者包含:多生产者模型(MultiProducerSequencer)和单生产者模型(SingleProducerSequencer)

这里以多生产者模型(MultiProducerSequencer)为例

    public <A> void publishEvent(EventTranslatorOneArg<E, A> translator, A arg0)
    {
        //获取当前能生产数据的序号
        final long sequence = sequencer.next();
        //往这个序号生产数据,其实就是为已经实例化的对象赋值。
        translateAndPublish(translator, sequence, arg0);
    }
    public long next(int n)
    {
        if (n < 1)
        {
            throw new IllegalArgumentException("n must be > 0");
        }

        long current;
        long next;

        do
        {
            current = cursor.get();
            next = current + n;
            // 可能构成环路的点/环形缓冲区可能追尾的点 = 请求的序号 - 环形缓冲区大小
            long wrapPoint = next - bufferSize;
            // 缓存的消费者们的最慢进度值,小于等于真实进度
            long cachedGatingSequence = gatingSequenceCache.get();
            
            // 第一步:空间不足就继续等待。
            // 1.wrapPoint > cachedGatingSequence 表示生产者追上消费者产生环路,上次看见的序号缓存无效,即缓冲区已满,此时需要获取消费者们最新的进度,以确定是否队列满。
            // 2.cachedGatingSequence > current   表示消费者的进度大于当前生产者进度,表示current无效,有以下可能:
            // 2.1 其它生产者发布了数据,并更新了gatingSequenceCache,并已被消费(当前线程进入该方法时可能被挂起,重新恢复调度时看见一个更大值)。
            // 2.2 claim的调用(建议忽略)

            if (wrapPoint > cachedGatingSequence || cachedGatingSequence > current)
            {
                // 走进这里表示cachedGatingSequence过期或current过期,此时都需要获取最新的gatingSequence
                long gatingSequence = Util.getMinimumSequence(gatingSequences, current);

                // 消费者最新的进度仍然与当前生产者构成了环路,那么只能重试
                if (wrapPoint > gatingSequence)
                {
                    // wrapPoint > gatingSequence 意味着 gatingSequence无效,因为生产者期待的是一个大于等于wrapPoint的值,因此也就不更新缓存。
                    LockSupport.parkNanos(1); // TODO, should we spin based on the wait strategy?
                    continue;
                }

                // 检测到未构成环路(多线程下这都是假设条件),更新网关序列,然后进行重试
                // 这里存在竞态条件,多线程模式下,可能会被设置为多个线程看见的结果中的任意一个,可能会被设置为一个更小的值,从而小于当前的查询值
                gatingSequenceCache.set(gatingSequence);
                // 这里看见有足够空间,这里如果尝试竞争空间会产生重复的代码,其实就是外层的代码,因此直接等待下一个循环
            }
            else if (cursor.compareAndSet(current, next))
            {
                // 第三步:成功竞争到了这片空间,返回
                // 注意!这里更新了生产者进度,然而生产者并未真正发布数据。
                // 因此消费者需要调用getHighestPublishedSequence()确认真正的可用空间
                break;
            }
        }
        while (true);

        return next;
    }

便于理解生产消费如何获取一段空间可参考:blog.csdn.net/zhouzhenyon…

应用场景

使用方式

简单使用就直接封装一个工具类出来,想要扩展性,可以参考shenyu封装一个成jar包(一个依赖)。

参考地址:github.com/apache/incu…

总结

最后对上面的内容做一个小结:

首先总结了它高性能的原因,列出了相关类以及属性(这块有助于看源码的时候快速理解相关代码)。

然后给出了demo示例,介绍了disruptor的使用,大家可以根据这个demodebug源码。

最后根据demo示例剖析了disruptor的初始化过程,以及消费者的注入和开启,最后到生产者如何生产数据(并没有达到庖丁解牛,只是抛砖引玉)。消费者使用sequenceBarrier.waitFor(nextSequence);方法,通过序号栅栏SequenceBarrier和序号Sequence搭配使用,协调和管理消费者与生产者的工作节奏,避免了锁和CAS的使用。

中间以及结尾穿插了很多网址,这些网址对大家理解disruptor源码,以及它的企业级使用有很大帮助。

最后讲一下学习disruptor的好处:如果你的项目刚好有它的使用场景,就可以赶紧用起来。如果使用disruptor出现问题,可以快速定位。可以借鉴它的思想来实现自己程序的优化。最后面试的时候还能和面试官吹一吹,岂不美哉!

继续深入学习可以参考:

参考的源码中文注释项目地址:github.com/hl845740757…

doutub_img.png

微信公众号「 袋鼠先生的客栈 」,有问题评论区见。如果你觉得我的分享对你有帮助,或者觉得我有两把刷子,就支持一下我这个干货writer吧,三连,三连,三连就是我最大的动力~,接下来会持续分享干货~(应该是k8s相关知识)。点赞👍 关注❤️ 分享👥