前几篇文章,分别介绍了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实现SingleProducerSequencer和MultiProducerSequencer(之间的却别还记得吗?嘻嘻嘻),查看各自的源码实现,其实两种模式最终调用的都是抽象类AbstractSequencer的newBarrier方法
public SequenceBarrier newBarrier(Sequence... sequencesToTrack)
{
// 持有waitStrategy -> 等待策略
// 持有cursor -> 核心对象,游标,表示当前生产者的生产位置,不同生产者,更新方式不同哦
// 持有sequencesToTrack -> 暂时忽略,链式调用用到的,依赖的消费者序号
return new ProcessingSequenceBarrier(this, waitStrategy, cursor, sequencesToTrack);
}
所以,最终就跟踪到ProcessingSequenceBarrier对象啦。
- ProcessingSequenceBarrie
老规矩,核心逻辑当然先看类图,核心方法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那样复杂。
框架优点很多,基于内存的高效队列。
缺点的话,最近做这个系列分享也想了一些,比如所有的数据都在内存中,服务重启的时候,其实可能会有丢数据的风险,就需要业务自己来实现补偿机制,保证服务重启后,丢失的数据可以再次写入生产者。业务架构整体设计下来,会比单纯的使用消息对列的方式更加复杂。
不过有得有失,基于内存设计,会比网络传输的中间接,节省部分耗时。
没有完美的架构,没有完美的解决方案,契合大家各自的业务,就是合理的架构。
如果看完文章,大家有一定的收获,可以随手点个赞哦~