在Spring中使用什么样的“EventBus”?内置,Reactor,Akka?

时间:2013-12-18 16:56:51

标签: multithreading spring akka event-driven-design project-reactor

我们将在几周内开始一个新的Spring 4应用程序。我们想使用一些事件驱动的架构。今年我在这里和那里读到关于“Reactor”的信息,在网上搜索时,我偶然发现了“Akka”。

所以现在我们有3个选择:

我无法找到真正的比较。


现在我们只需要:

  • X注册以收听Event E
  • Y注册以收听Event E
  • Z发送Event E

然后XY将接收并处理该事件。

我们很可能会以异步方式使用它,但肯定会有一些同步方案。我们很可能总是将一个类作为事件发送。 (Reactor示例主要使用字符串和字符串模式,但它也支持对象)。


据我所知,ApplicationEvent默认情况下同步,Reactor以异步方式工作。并且Reactor也允许使用await()方法使其有点同步。 AkkaReactor或多或少相同,但也支持远程处理。

关于Reactor的await()方法:它可以等待多个线程完成吗?或者甚至可能是这些线程的一部分?如果我们从上面举例:

  • X注册以收听Event E
  • Y注册以收听Event E
  • Z发送Event E

是否可以通过以下方式使其同步:等待X Y完成。是否可以让它等待X,而不是等待Y


也许还有其他选择?例如JMS呢?

很多问题,但希望你能提供一些答案!

谢谢!


编辑:示例用例

  1. 当特定事件被触发时,我想创建10000封电子邮件。每封电子邮件都必须使用特定于用户的内容生成。所以我创建了很多线程(max =系统cpu核心)来创建邮件并且不阻塞调用者线程,因为这可能需要几分钟。

  2. 当特定事件被触发时,我想从未知数量的服务中收集信息。每次获取大约需要100毫秒。在这里,我可以想象使用Reactor的await,因为我需要这些信息来继续我在主线程中的工作。

  3. 当特定事件被触发时,我想根据应用程序配置执行一些操作。因此,应用程序必须能够动态(un)注册comsumers /事件处理程序。他们会用事件做自己的事情,我不在乎。所以我会为每个处理程序创建一个线程,然后继续在主线程中完成我的工作。

  4. 简单解耦:我基本上知道所有接收器,但我只是不想在我的代码中调用每个接收器。这应该主要是同步完成的。

  5. 听起来像我需要一个ThreadPool或一个RingBuffer。这些框架是否具有动态RingBuffers,如果需要,它会增大?

4 个答案:

答案 0 :(得分:28)

我不确定我能否在这个狭小的空间内充分回答你的问题。但我会试一试! :)

就功能而言,Spring的ApplicationEvent系统和Reactor非常不同。 ApplicationEvent路由基于ApplicationListener处理的类型。任何比这更复杂的东西,你必须自己实现逻辑(但这不一定是坏事)。但是,Reactor提供了一个全面的路由层,它也非常轻量级且完全可扩展。两者在订阅和发布事件的能力方面的功能相似,这实际上是任何事件驱动系统的一个特征。另外,不要忘记使用Spring 4的新spring-messaging模块。它是Spring Integration中可用工具的一个子集,还提供了围绕事件驱动架构构建的抽象。

Reactor将帮助您解决一些您必须自己管理的关键问题:

选择器匹配:Reactor进行Selector匹配,包含一系列匹配 - 从简单的.equals(Object other)调用到更复杂的URI模板匹配,允许占位符提取。您还可以使用自己的自定义逻辑扩展内置选择器,以便可以使用富对象作为通知键(例如域对象)。

流和提交API :您已经参考Promise方法提到了.await() API,这实际上是针对需要阻止行为的现有代码。使用Reactor编写新代码时,不能强调使用组合和回调来有效利用系统资源而不阻塞线程。在依赖于少量线程来执行大量任务的体系结构中,阻止调用者几乎不是一个好主意。期货根本不具有云可扩展性,这就是现代应用程序利用替代解决方案的原因。

您的应用程序可以使用Streams或Promises构建一个,但老实说,我认为您会发现Stream更灵活。关键的好处是API的可组合性,它允许您在依赖链中将操作连接在一起而不会阻塞。作为一个基于您的电子邮件用例的完全袖手旁观的例子,您描述了:

@Autowired
Environment env;
@Autowired
SmtpClient client;

// Using a ThreadPoolDispatcher
Deferred<DomainObject, Stream<DomainObject>> input = Streams.defer(env, THREAD_POOL);

input.compose()
  .map(new Function<DomainObject, EmailTemplate>() {
    public EmailTemplate apply(DomainObject in) {
      // generate the email
      return new EmailTemplate(in);
    }
  })
  .consume(new Consumer<EmailTemplate>() {
    public void accept(EmailTemplate email) {
      // send the email
      client.send(email);
    }
  });

// Publish input into Deferred
DomainObject obj = reader.readNext();
if(null != obj) {
  input.accept(obj);
}

Reactor还提供Boundary,它基本上是CountDownLatch,用于阻止任意消费者(因此,如果您只想阻止,则不必构造Promise完成Consumer。在这种情况下,您可以使用原始Reactor并使用on()notify()方法来触发服务状态检查。

但是,对于某些内容,似乎您想要的是从Future返回的ExecutorService,不是吗?为什么不保持简单?只有在您的吞吐量性能和开销效率很重要的情况下,Reactor才会真正受益。如果您正在阻止调用线程,那么您可能会消除Reactor将为您提供的效率增益,因此在这种情况下使用更传统的工具集可能会更好。

关于Reactor开放性的好处在于,没有什么可以阻止两者进行交互。您可以自由地将FuturesConsumers混合而不会产生静态。在这种情况下,请记住,您只会像最慢的组件一样快。

答案 1 :(得分:6)

让我们忽略Spring的ApplicationEvent,因为它实际上不是为你的要求而设计的(更多关于bean生命周期管理)。

你需要弄清楚的是你想要做什么

  1. 面向对象的方式(即演员,动态消费者,即时注册) OR
  2. 服务方式(静态消费者,在启动时注册)。
  3. 使用XY的示例是:

    1. 短暂的实例(1)或是
    2. 长寿的单身人士/服务对象(2)?
    3. 如果您需要在飞行中注册消费者,那么Akka是一个不错的选择(我不确定反应堆,因为我从未使用它)。如果您不想在短暂的对象中使用JMS或AMQP。

      您还需要了解这些类型的库正试图解决两个问题:

      1. 并发(即在同一台机器上并行处理)
      2. 分发(即在多台机器上并行处理)
      3. Reactor和Akka主要关注#1。 Akka最近刚刚添加了集群支持,而actor抽象使得#2更容易实现。消息队列(JMS,AMQP)专注于#2。

        对于我自己的工作,我做服务路线并使用经过大量修改的Guava EventBus和RabbitMQ。我使用类似于Guava Eventbus的注释,但也有在总线上发送的对象的注释,但是你可以在异步模式下使用Guava的EventBus作为POC,然后像我一样自己创建。

        您可能认为您需要拥有动态消费者(1),但大多数问题都可以通过简单的发布/订阅来解决。管理动态消费者也很棘手(因此,Akka是一个不错的选择,因为演员模型对此有各种管理)

答案 2 :(得分:3)

从框架中仔细定义您想要的内容。如果框架具有比您需要的更多功能,那么它并不总是好的。更多功能意味着更多错误,更多代码需要学习,以及更低的性能。

值得关注的一些功能是:

  • 演员(线程或轻量级对象)的本质
  • 在计算机集群上工作的能力(Akka)
  • 持久性消息队列(JMS)
  • 特定功能,如信号(没有信息的事件),转换(将来自不同端口的消息组合成复杂事件的对象,请参阅Petri网)等。

小心使用像await这样的同步功能 - 它会阻塞整个线程,并且在线程池上执行actor(线程饥饿)时会很危险。

要查看更多框架:

Fork-Join Pool - 在某些情况下,允许await没有线程饥饿

Scientific workflow systems

Dataflow framework for Java - 信号,过渡

ADD-ON :两种演员。

通常,并行工作系统可以表示为图形,其中活动节点相互发送消息。在Java中,与大多数其他主流语言一样,活动节点(actor)可以作为线程池执行的线程或任务(Runnable或Callable)实现。通常,部分演员是线程,部分是任务。这两种方法都有其优点和缺点,因此为系统中的每个参与者选择最合适的实现至关重要。 简而言之,线程可以阻塞(并等待事件),但会为其堆栈消耗大量内存。任务可能不会阻塞,但使用共享堆栈(池中的线程)。

如果任务调用阻塞操作,则会从服务中排除池化线程。如果许多任务阻塞,它们可以排除所有线程,从而导致死锁 - 那些可以解除阻塞阻塞任务的任务无法运行。这种死锁称为线程饥饿。如果,为了防止线程饥饿,将线程池配置为无限制,我们只需将任务转换为线程,从而失去任务的优势。

要消除对任务中阻塞操作的调用,任务应分为两个(或更多) - 第一个任务调用阻塞操作和退出,其余的格式化为阻塞操作完成时启动的异步任务。当然,阻塞操作必须具有备用异步接口。因此,例如,不应同步读取套接字,而应使用NIO或NIO2库。

不幸的是,标准的java库缺少流行同步工具(如队列和信号量)的异步对应物。幸运的是,从头开始很容易实现(例如,参见Dataflow framework for Java)。

因此,纯粹使用非阻塞任务进行计算是可能的,但会增加代码的大小。显而易见的建议是尽可能使用线程,只为简单的大规模计算使用任务。

答案 3 :(得分:0)

一个简单的同步事件:1.应用程序事件,2.发布者,3.侦听器

public class SyncSpringEvent extends ApplicationEvent {

private String messageRequest;

public SyncSpringEvent(Object source, String messageRequest) {
super(source);
this.messageRequest = messageRequest;
}
public String getMessageRequest() {
return messageRequest;
}
}


@Component
public class SyncSpringEventPublisher {
@Autowired
private ApplicationEventPublisher applicationEventPublisher;

public void doStuffAndPublishAnEvent(final String messageRequest) { 
SyncSpringEvent syncSpringEvent = new SyncSpringEvent(this, messageRequest);
applicationEventPublisher.publishEvent(syncSpringEvent);
}
}

@Component
public class SyncSpringEventListener implements ApplicationListener<SyncSpringEvent> {
@Override
public void onApplicationEvent(SyncSpringEvent event) {
System.out.println("Received spring request event - " + event.getMessageRequest());
}
}