5、Disruptor原理解析-序号屏障

235 阅读2分钟

前几篇文章,分别介绍了RingBuffer基本原理、生产者如何生产、消费者如何消费,以及序号器的作用。介绍序号器的时候提到了“SequenceBarrier”,故名思义,序号屏障,是用来保证消费者进度和生产进度之间的“屏障”,放逐出现消费者“跑”到了生产者前面,这篇文章将会带大家一起分析下其工作原理。

一、温故知新

介绍其工作原理之前,先让我们回顾下,SequenceBarrier是如何在消费端起作用的。通过前文分析可得知,Disruptor提供了两种消费模式,分别是BatchEventProcessor和WorkProcessor,二者的区别可参考前文

  • BatchEventProcessor
    EventHandlerGroup<T> createEventProcessors(final Sequence[] barrierSequences,final EventHandler<? super T>[] eventHandlers){

        ... ...
        final SequenceBarrier barrier = ringBuffer.newBarrier(barrierSequences);

        for (int i = 0, eventHandlersLength = eventHandlers.length; i < eventHandlersLength; i++)
        {

            ... ... 
            final BatchEventProcessor<T> batchEventProcessor =
                new BatchEventProcessor<T>(ringBuffer, barrier, eventHandler);
                
            ... ...
        }
    }

通过调用ringBuffer.newBarrier,获取到的SequenceBarrier实例,同时传递给BatchEventProcessor对象

    public void run()
    {
        ... ...
        try
        {
            while (true)
            {
                try
                {
                    // 实际应用
                    final long availableSequence sequenceBarrier.waitFor(nextSequence);

                    while (nextSequence <= availableSequence)
                    {
                        event = dataProvider.get(nextSequence);
                        eventHandler.onEvent(event, nextSequence, nextSequence == availableSequence);
                        nextSequence++;
                    }
                    ... ...
                }
 
            }
        }
        finally
        {
        }
    }

调用sequenceBarrier.waitFor方法,指定自己想要获取的序号边界,从而获取到自己最大能消费的位置。

  • WorkProcessor 其实现方法和上述类似,通过ringBuffer.newBarrier(barrierSequences)获取SequenceBarrier,其内部也是调用sequenceBarrier.waitFor方法,获取自己能消费的序号边界。

二、详解sequenceBarrier

1.创建

    public SequenceBarrier newBarrier(Sequence... sequencesToTrack)
    {
        // sequencesToTrack对象先忽略,当前理解是链式调用才会用到,后面会分析
        return sequencer.newBarrier(sequencesToTrack);
    }

调用RingBuffer.newBarrier方法,内部sequencer对象来创建对应的序号屏障。

阅读过前文的同学想必还记得,RingBuffer其实提供了两种生产模式,分别对应两种sequencer实现SingleProducerSequencerMultiProducerSequencer(之间的却别还记得吗?嘻嘻嘻),查看各自的源码实现,其实两种模式最终调用的都是抽象类AbstractSequencer的newBarrier方法

 public SequenceBarrier newBarrier(Sequence... sequencesToTrack)
    {
        // 持有waitStrategy      -> 等待策略
        // 持有cursor            -> 核心对象,游标,表示当前生产者的生产位置,不同生产者,更新方式不同哦
        // 持有sequencesToTrack  -> 暂时忽略,链式调用用到的,依赖的消费者序号
        return new ProcessingSequenceBarrier(this, waitStrategy, cursor, sequencesToTrack);
    }

所以,最终就跟踪到ProcessingSequenceBarrier对象啦。

  • ProcessingSequenceBarrie

image.png 老规矩,核心逻辑当然先看类图,核心方法rwaitFor,同时依赖WaitStrategy的多个实现类。

  • waitFor
    public long waitFor(final long sequence)
        throws AlertException, InterruptedException, TimeoutException
    {
        checkAlert();

        // 通过等待策略,获取小于cursorSequence自己可用的序号
        long availableSequence = waitStrategy.waitFor(sequence, cursorSequence, dependentSequence, this);

        // 当前序号小于自己要获取的边界需要,说明有数据可以消费,则直接用
        if (availableSequence < sequence)
        {
            return availableSequence;
        }

        // 如果不小于,返回所有sequence(可能多生产者)和availableSequence中最大的序号
        // 算是优化吧,多生产情况下,消费者原想要消费第三个槽上的数据,实际已经写到了第六个槽,那就直接告诉你,你能消费到第六个槽了,不用在通过下一次的竞争和等待来获取了
        return sequencer.getHighestPublishedSequence(sequence, availableSequence);
    }

2.等待策略

  • BlockingWaitStrategy:用了ReentrantLock的等待&&唤醒机制实现等待逻辑,是默认策略,比较节省CPU
  • BusySpinWaitStrategy:持续自旋,JDK9之下慎用(最好别用)
  • DummyWaitStrategy:返回的Sequence值为0,正常环境是用不上的
  • LiteBlockingWaitStrategy:基于BlockingWaitStrategy,在没有锁竞争的时候会省去唤醒操作,但是作者说测试不充分,不建议使用
  • TimeoutBlockingWaitStrategy:带超时的等待,超时后会执行业务指定的处理逻辑
  • LiteTimeoutBlockingWaitStrategy:基于TimeoutBlockingWaitStrategy,在没有锁竞争的时候会省去唤醒操作
  • SleepingWaitStrategy:三段式,第一阶段自旋,第二阶段执行Thread.yield交出CPU,第三阶段睡眠执行时间,反复的的睡眠
  • YieldingWaitStrategy:二段式,第一阶段自旋,第二阶段执行Thread.yield交出CPU
  • PhasedBackoffWaitStrategy:四段式,第一阶段自旋指定次数,第二阶段自旋指定时间,第三阶段执行Thread.yield交出CPU,第四阶段调用成员变量的waitFor方法,这个成员变量可以被设置为BlockingWaitStrategy、LiteBlockingWaitStrategy、SleepingWaitStrategy这三个中的一个

Disruptor提供了九种等待策略,当然也可以自己实现WaitStrategy类。至于为什么要提供等待策略,大家可以想一个场景,当前消费者太勤劳了,生产者已经供不应求了,那么消费者应该用种姿势等到消息过来呢,CPU空转?还是定时轮训?等待策略就是为了解决这种场景的。

以BlockingWaitStrategy为栗

public final class BlockingWaitStrategy implements WaitStrategy
{
    private final Lock lock = new ReentrantLock();
    private final Condition processorNotifyCondition = lock.newCondition();

    @Override
    public long waitFor(long sequence, Sequence cursorSequence, Sequence dependentSequence, SequenceBarrier barrier)
        throws AlertException, InterruptedException
    {
        long availableSequence;
        if (cursorSequence.get() < sequence)
        {
            lock.lock();
            try
            {   // 当前生产者的生产速度,已经小于消费者要消费的速度
                while (cursorSequence.get() < sequence)
                {
                    barrier.checkAlert();
                    // 等待
                    processorNotifyCondition.await();
                }
            }
            finally
            {
                lock.unlock();
            }
        }

        // 当前生产者加班加点终于赶上消费者进度了,但是依赖的其他消费者还没处理完,则等待所依赖的消费者先处理
        while ((availableSequence = dependentSequence.get()) < sequence)
        {
            barrier.checkAlert();
        }

        // 返回可用的序号
        return availableSequence;
    }

    // 有等待当然有唤醒了
    // 一种是在生产者有数据发布的时候的时候,即sequence.publish的时候,是不是和前卫串起来了
    // 一直是调用next方法,发现ringBuffer满了,这个时候也要尝试去唤醒下等待的消费者,提醒他们赶紧起床干活了
    @Override
    public void signalAllWhenBlocking()
    {
        lock.lock();
        try
        {
            processorNotifyCondition.signalAll();
        }
        finally
        {
            lock.unlock();
        }
    }

    @Override
    public String toString()
    {
        return "BlockingWaitStrategy{" +
            "processorNotifyCondition=" + processorNotifyCondition +
            '}';
    }
}

代码其实很简洁,内部通过lock对象,以及processorNotifyCondition对象,如果消费过快,则进行等待。当有新的消息发布的时候、或者生产者发现RingBuffer已经满了的时候,通知等待的消费者,赶紧进行消费。

3.getHighestPublishedSequence

当消费者获取到的可消费序号,大于自己想要申请的序号,即生产者可能很快生产了一批消费。此时消费者应该尽可能的消费数据,而不是按部就班每次还来竞争锁再决定自己能消费的进度。由于Disruptor提供了两种生产模式,所以这个方法肯定对应两种实现啦

  • MultiProducerSequencer
   
   public long getHighestPublishedSequence(long lowerBound, long availableSequence)
    {
        // lowerBound         -> 消费者想要消费的位置
        // availableSequence  -> 消费者实际获取到的位置
        // lowerBound < availableSequence
        // 遍历,如果发现当前的槽,还没生产者写入呢,说明已经找到了生产者生产的边界,返回
        for (long sequence = lowerBound; sequence <= availableSequence; sequence++)
        {
            // isAvailable前文分析过了,这里不展开了
            if (!isAvailable(sequence))
            {
                return sequence - 1;
            }
        }

        // 遍历到availableSequence,之前所有的槽,都有生产者写入了,那么,都给这个消费者来消费吧
        return availableSequence;
    }
  • SingleProducerSequencer
   public long getHighestPublishedSequence(long lowerBound, long availableSequence)
    {
        // 单生产者就简单粗暴了,根本没这个问题,我写到哪,你就用到哪
        return availableSequence;
    }

三、小结

完结撒花🎉🎉🎉🎉🎉🎉🎉🎉🎉🎉

Disruptor系列博客应该就到这里,整体代码阅读下来,比较清晰,不像netty和spring那样复杂。

框架优点很多,基于内存的高效队列。

缺点的话,最近做这个系列分享也想了一些,比如所有的数据都在内存中,服务重启的时候,其实可能会有丢数据的风险,就需要业务自己来实现补偿机制,保证服务重启后,丢失的数据可以再次写入生产者。业务架构整体设计下来,会比单纯的使用消息对列的方式更加复杂。

不过有得有失,基于内存设计,会比网络传输的中间接,节省部分耗时。

没有完美的架构,没有完美的解决方案,契合大家各自的业务,就是合理的架构。

如果看完文章,大家有一定的收获,可以随手点个赞哦~