原文地址http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-wiring-up.html
那么现在我已经涵盖了ring buffer,从它读取和向它写入。
从逻辑上来说,下一件要做的事情是把所有东西拼在一起。
我已经讲过了多生产者的情况-它们通过producer barrier来保持顺序和可控。我也讲了关于一个简单情形下的消费者。多消费者会复杂一些。我们做了一些聪明的事来让消费者可以依赖彼此和ring buffer。和许多应用一样,我们有一管的东西需要在开始业务逻辑之前完成-比如,在我们可以做任何事情之前需要确认消息已经被记录到了硬盘。
Disruptor文章和性能测试中包含一些你可能需要的基本配置。我将会重新讲解最有趣的那个,多半因为我需要练习使用画图板。
钻石配置
DiamondPath1P3CPerfTest示范了一个不是很罕见的配置-一个单独的生产者和三个消费者。棘手的是第三个消费者只有前两个完成之后才能开工。
3号消费者可能是你的业务逻辑,1号消费者可能正在备份接收到的数据,而2号消费者可能正在准备数据或者别的什么。
使用队列的钻石配置
在一个SEDA式架构中,每个阶段都会用队列隔开:
(为什么queue中非要有那么多“e”呢?这是在我的图里用到的所有单词中最难画的)。
你可能会隐约发现这个问题:对于一条从P1到C3的消息,它需要通过四个队列,每个队列都要在把消息放到队列和从队列中取出消息上耗费成本。
使用Disruptor的钻石配置
在Disruptor世界里,这些全都由单个ring buffer管理:
这看起来确实更复杂一些。但是ring buffer维护所有参与者的唯一交互点,并且这些行为全部基于那些Barrier检查自己所依赖东西的序列号的操作。
生产者这边比较简单,这是我上一篇文章中提过的单生产者模型。有趣的是,producer barrier不需要关心所有消费者们。它只关心3号消费者,因为如果3号消费者已经完成,并且已经有一个项在ring buffer中了的话,那么另两个消费者就肯定已经完成了。因此如果C3已经前进,它所在的槽在ring buffer中就是可用的。
为了管理这些消费者之间的依赖关系,你需要两个consumer barrier。第一个就和ring buffer对话,并且1号和2号消费者向它请求下一个可用项。第二个consumer barrier了解1号和2号消费者,并且它会返回两个消费者生产的序列号中较小的那个。
Disruptor中的消费者依赖关系是怎么工作的
嗯。我想我需要一个例子。
我们在这个故事进行一半的时候加入进来:生产者已经把ring buffer填充到了序列号22,1号消费者已经读取和完成了到21为止的所有东西;2号消费者已经完成了到序列号18为止的所有东西;3号消费者,依赖于前两个消费者,只到达15。生产者无法向ring buffer写入任何东西因为序列15正占据着我们想放序列23的这个槽。
(抱歉,我真的试过找其他颜色来替代红色和绿色,但是它们都很容易混淆)。
第一个consumer barrier通知1号和2号消费者它们可以抢占序列22之前的所有东西,这是ring buffer中的最高序列号。第二个consumer barrier检查ring buffer的序列,不过它也会检查另外两个消费者者的序列号并返回较小的那个值。因此3号消费者被告知可以从ring buffer中获取到序列18为止的所有东西。
注意这些消费者仍然是直接从ring buffer读取条目的-1号和2号消费者不是把条目从ring buffer中拿出来再传给3号消费者。相反的,是第二个consumer barrier让3号消费者知道ring buffer中的哪个条目可以安全地拿来处理。
这引发一个疑问-如果所有东西都脱离ring buffer直接进行,3号消费者怎么找出1号和2号消费者所做的一切?如果所有3号消费者需要关心的只是较早的那个消费者是否已经完成了它们的工作(比如把数据复制到其他地方)那么一切都没问题了-当3号消费者被告知它们已经完成工作,它会很高兴。但是如果,3号消费者需要一个较早消费者的处理结果,它从哪里拿这个结果呢?
编辑条目
秘诀就是由它自己向ring buffer的条目写这些。这样的话,当3号消费者从ring buffer抢占到条目时,它就会获得所有做工作所需的信息。
Entry中的每个字段只能允许一个消费者写入,这点真的很重要。这样可以避免任何写冲突,写冲突会使整个事情慢下来。
你可以在 DiamondPath1P3CPerfTest中看到这个- FizzBuzzEntry有两个字段:fizz和buzz。Fizz消费者写到fizz。Buzz消费者写到buzz。第三个消费者,FizzBuzz会读这两个字段,但是不会向其中任何一个写入,因为读是好的,不会引起冲突。
一些实际的Java代码
这些看起来比用队列来实现要复杂。是的,它确实牵涉到更多一点的协作。但是这对于消费者和生产者是隐藏的,它们只和barrier对话。这个花招在配置里。上面例子中的钻石图会用类似下面的方法来创建:
ConsumerBarrier consumerBarrier1 = ringBuffer.createConsumerBarrier();
BatchConsumer consumer1 = new BatchConsumer(consumerBarrier1, handler1);
BatchConsumer consumer2 = new BatchConsumer(consumerBarrier1, handler2);
ConsumerBarrier consumerBarrier2 =
ringBuffer.createConsumerBarrier(consumer1, consumer2);
BatchConsumer consumer3 = new BatchConsumer(consumerBarrier2, handler3);
ProducerBarrier producerBarrier =
ringBuffer.createProducerBarrier(consumer3);
总结
那么现在你学会了-如何拼接Disruptor和相互依赖的多消费者。关键点:
使用多个consumer barrier来管理消费者之间的依赖关系。
让producer barrier来关注图中的最后一个消费者。
只允许一个消费者向Entry中的单独字段写入。
修改:Adrian写了一个很好的DSL来使得拼接Disruptor更容易。
修改2:注意2.0版的Disruptor使用了和本文提到的不一样的名称。如果你对类名有疑问,请查看我的变更总结。还有Adrain的DSL现在也是Disruptor代码库的主要部分了。
部分评论:
Adrian SuttonJul 11, 2011 12:34 AM
如果你在拼接你的消费者时想少写一些代码,请看一下disruptor dsl:
http://github.com/ajsutton/disruptorWizard
它简化了拼接甚至是最复杂的消费者模式,通过传递批量的处理器,由dsl关心创建实际的consumer,barrier和保证producer barrier阻塞在消费者链尾部。