Disruptor 全解析(4):拼接依赖原文地址:http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-wi
Disruptor 全解析(4):拼接依赖
原文地址:http://mechanitis.blogspot.com/2011/07/dissecting-disruptor-wiring-up.html??作者是 Trisha Gee, LMAX 公司的一位美女工程师
现在我已经讲过了?RingBuffer??本身,从它?读取? 以及向它?写入?。
从逻辑上来说,下一件要做的事情就是把所有的东西拼接在一起。
我提到了多生产者的情况——他们通过 ProducerBarrier 保持操作顺序与可控。我也提过多个消费者在一个简单场景的情况。多个消费者的情况会变得有点儿更复杂,我们? 实现了一些聪明的机制允许多个消费者与 RingBuffer?之间互相依赖。像在很多应用,有一连串的事情需要在实际执行业务逻辑之前发生 (happen before)——例如,做任何操作之前,我们必须先确保消息已经被写到磁盘。
Disruptor 论文? 和 性能测试 里包含了你可能想要的一些基本结构。我准备讲一下最有趣的那个,这多半是因为我需要练习如何使用绘图板。
钻石结构DiamondPath1P3CPerfTest? 展示了一个并不非常罕见的结构——单独的一个生产者和三个消费者。最棘手的一点是:第三个消费者依赖前两个消费者处理结束后,才能开始工作。
三号消费者可能是你的业务逻辑。一号消费者可能正在备份接收到的数据,而二号消费者可能正在准备数据或者其他的。
用队列实现的钻石结构在一个?SEDA-风格的架构? 中,每个阶段都会用队列隔开:
(为什么 Queue 必须有这么多 "e" 呢?这是我画这些图中遇到的最麻烦的词)。
你也许从这看到了问题的端倪:一条消息从 P1 到 C3 要穿过四个完整的队列,每个队列都在消息进入队列和取出队列上消耗成本。
用 Disruptor 实现的钻石结构在?Disruptor? 的世界里,一切都由单个 RingBuffer 管理:
它看起来更复杂。但 RingBuffer 保持作为一个单点联系所有的参与者,并且所有交互都基于屏障 (Barriers) 检查它所依赖的对象序号来实现。
生产者这边比较简单,它是我在?上文? 中描述过的单生产者模型。有趣的是,生产者并不需要关心所有的消费者。它只关心三号消费者,因为如果三号消费者处理完 RingBuffer 中的一个节点,另外两个消费者肯定也处理完了。因此如果 C3 向前走,RingBuffer 的节点就空余出来了。
管理这些消费者的依赖关系需要两个 ConsumerBarrier 对象。第一个仅仅与 ?RingBuffer 交流,一号和二号消费者向它请求下一个可用节点。第二个 ConsumerBarrier 只知道一号和二号消费者,并且它返回两个消费者处理的消息序号中较小的那个。
消费者依赖怎样在 Disruptor 下工作嗯。我想我需要一个例子。
我们从这个故事发生到一半的时候来看:生产者 (P1) 已经写入 RingBuffer 到序号 22 了,一号消费者 (C1) 已经读和处理完了序号 21 之前的所有数据。二号消费者 (C2) 处理到了 18。三号消费者 (C3),就是依赖其他消费者的那个,才处理到 15。
生产者不能再向 RingBuffer 写数据了,因为序号 15 占据了我们想要写入序号 23 的节点 (Slot)。
(抱歉,我真的试过用其他颜色来代替红色和绿色,但是其它的都容易混淆。)
第一个 ConsumerBarrier?(CB1) 告诉一号和二号消费者可以去拿 22 号前面的所有东西,这是 RingBuffer 当前的最高序号。第二个 ConsumerBarrier (CB2) 不但检查 RingBuffer 的序号,也会检查另外两个消费者的序号并返回最小值。因此,三号消费者被告知可以获取 RingBuffer 序号 18 之前的所有东西。
注意这些消费者仍然直接从 RingBuffer 获取节点——并不是由一号和二号消费者把节点从 RingBuffer 取出然后传递给三号消费者的。作为代替,第二个 ConsumerBarrier 告诉三号消费者,RingBuffer 中哪些节点可以安全的处理。
这产生了一个问题——如果任何东西都来自 RingBuffer,那么三号消费者怎样拿到前两个消费者所完成的东西?如果三号消费者关心的只是早先的消费者是否已经完成它们的工作(例如,把数据复制到其他地方),这一切都很好——当三号消费者收到工作已完成,它就开心了。但是,如果三号消费者需要早先消费者的处理结果,它从哪里获取呢?
修改节点秘密在于把处理结果写入 RingBuffer 节点 (Entry) 自身。这样,当三号消费者从 RingBuffer 取出节点时,它已经填好了三号消费者工作需要的所有信息。这里?
真正?重要的部分是节点 (Entry) 的每个字段只允许一个消费者写入。这避免了造成写争用 (write-contention) 减缓整个处理过程。
你可以在?DiamondPath1P3CPerfTest? 看到这个例子——?FizzBuzzEntry? 有两个字段和值:fizz 和 buzz。如果消费者是 Fizz Consumer, 它只写 fizz。如果是 Buzz Consumer, 它只写 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 与相互依赖的多个消费者。关键点是:?※ 使用多个 ConsumerBarrier 来管理消费者之间的依赖关系。?※?使用 ProducerBarrier 监视结构图中的最后一个消费者。?※ 只允许有一个消费者更新节点 (Entry) 中的一个单独字段。
修改:Adrian 写了一个很好的?DSL?工具让拼接 Disruptor 更加简单了。
?修改 2:注意 Disruptor 2.0 使用了与这篇文章不一样的命名。如果你对类名感到困惑,请阅读我的?变更总结??。另外,Adrian 的 DSL 工具现在是 Disruptor 主干代码的一部分了。
?
本文翻译自原作者博客, 译者是博客园?﹎〾敏ō?(http://www.cnblogs.com/adaikiss/??????), 我只是转载并修改了若干字句, 特别感谢。 ?