AKKA中的事件流

在《企业应用集成模式》一书中,定义了许多与消息处理有关的模式,其中运用最为广泛的模式为Publisher-Subscriber模式,尤其是在异步处理场景下。

基于Publisher-Subscriber模式,还可以根据不同的场景衍生出特殊的模式,例如针对一个Publisher和多个Subscriber,演化为Broadcast模式和Message Router模式。前者会将消息同时发送给所有的Subscriber,实现分布式的并行处理。例如针对订单处理的场景,当顾客下订单后,既需要生成订单,又需要通知库存准备发货,还需要通知卖方和买方。这些任务虽然存在事务的一致性,但基于BASE原则,可以通过补偿机制实现事务的最终一致性。于是,设计时可以将这些任务交给不同的Subscriber,当接收到消息后,同时对订单进行处理。至于Message Router,则需要引入的Router对传入的消息作出智能判断,从而将消息传递给真正感兴趣的Subscriber。这就好像发布者同时发布了不同的刊物,订阅者只订阅自己喜欢的刊物。

而消息总线(message bus)则通过引入总线来彻底解除Publisher与Subscriber之间的耦合,类似设计模式中的Mediator模式。总线就是Mediator,用以协调Publisher与Subscriber之间的关系。或者,我们也可以认为是两个Publisher-Subscriber的组合。对于Publisher而言,总线就是Subscriber;对于Subscriber而言,总线则成了Publisher。

AKKA提供的事件总线(Event Bus)可以看做是一种运用于特殊场景的消息总线,此时事件即为消息。它可以看做是Message Router模式的实现,提供了向多个Actor发送消息的基础设施,内含的Classifier作为分类器,用于分发消息时选择Subscriber,扮演了Message Router的角色。

在AKKA中,Event Bus被定义为trait,定义了基本的订阅、取消订阅、发布等对应的方法,代码如下所示:

trait EventBus {
  type Event
  type Classifier
  type Subscriber  

  def subscribe(subscriber: Subscriber, to: Classifier): Boolean
  def unsubscribe(subscriber: Subscriber, from: Classifier): Boolean
  def unsubscribe(subscriber: Subscriber): Unit
  def publish(event: Event): Unit
}

根据AKKA官方文档的描述,Event为所有发布到该总线上的事件类型,Classifier是选择订阅者的分类器,Subscriber就是注册到该总线上的订阅者。它们均被定义为抽象的type,使得EventBus拥有最大的开放性。我们视情况而定去具体实现特定类型。例如针对Actor的EventBus,订阅者被指定为ActorRef类型:

trait ActorEventBus extends EventBus {
  type Subscriber = ActorRef
  protected def compareSubscribers(a: ActorRef, b: ActorRef) = a compareTo b
}

AKKA运用Cake Pattern,以trait类型定义了一些特化的分类器。例如:

trait ActorClassifier {
 this: EventBus ⇒  
 type Classifier = ActorRef
}

trait PredicateClassifier {
 this: EventBus ⇒  
 type Classifier = Event ⇒ Boolean
}

除此之外,还有诸如SubchannelClassification、ScanningClassification、ActorClassification等分类器,都采用了同样的方式定义为trait。这样就便于继承EventBus的类进行trait的混入,例如EventStream的定义:

class EventStream(private val debug: Boolean = false) extends LoggingBus with SubchannelClassification {
  type Event = AnyRef
  type Classifier = Class[_]
}

由于LoggingBus继承自ActorEventBus,故而EventStream中的Subscriber类型被定义为ActorRef。然后在EventStream中又重写了Event和Classfier类型,分别为AnyRef和Class,这说明任何Java引用对象都可以作为事件,而分类的依据则为Event的类型。

type Subscriber = ActorRef
type Event = AnyRef
type Classifier = Class[_]

EventStream继承了SubchannelClassification。在其中维持了订阅者列表,虽然该订阅列表类型为SubclassifiedIndex,不过我们可以将其简单地视为一个Map(实际情况更复杂,因为它实际上维护了分类的层级):

trait SubchannelClassification {
  this: EventBus ⇒
  private lazy val subscriptions = new SubclassifiedIndex[Classifier, Subscriber]()
}

由于此时的Classifier在EventStream中被定义为Class[_],Subscriber为ActorRef,因此subscripions相当于被定义为:

private lazy val subscriptions = new SubclassifiedIndex[Class[_], ActorRef]

当Actor出现故障,从而使得消息被转发给dead letter时,我们可能需要侦听这些死信,并对它们进行处理。则AKKA的做法就是通过EventStream来进行订阅:

class DeadLetterListener extends Actor {
  def receive = {
    case DeadLetter(msg, from, to) =>
      println(s"${System.currentTimeMillis()}: from $from to $to with message $msg.")
  }
}

val listener = system.actorOf(Props[DeadLetterListener], "listener")
system.eventStream.subscribe(listener, classOf[DeadLetter])

结合SubchannelClassification,我们来分析其执行过程。

首先,它通过subscribe方法将DeadLetterListener的actor引用对象以及事件类型DeadLetter注册到SubchannelClassification中的subscriptions。当ActorSystem的任意actor发出DeadLetter时,就会触发EventStream的publish()方法:

class EventStream(private val debug: Boolean = false) extends LoggingBus with SubchannelClassification {
  protected def publish(event: AnyRef, subscriber: ActorRef) = {
    if (subscriber.isTerminated) unsubscribe(subscriber)
    else subscriber ! event
  }
}

此时就会通过subscriber(此时为前面定义的DeadLetterListener)发送event(此时为DeadLetter对象),从而进入到DeadLetterListener的receive方法中,打印出我想要的消息。

通过EventStream还可以处理日志消息。AKKA自身也提供了默认的处理器,可以配置在application.conf文件中:

akka {
  event-handlers = ["akka.event.Logging$DefaultLogger"]
}

这个默认的日志处理器会订阅高于配置级别的日志事件类,例如将日志级别配置为Debug:

system.eventStream.setLogLevel(Logging.DebugLevel)

通过这样的配置,所有低于Debug级别的日志事件发生时,都不会被EventStream分发。

原文发布于微信公众号 - 逸言(YiYan_OneWord)

原文发表时间:2015-09-25

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏林冠宏的技术文章

关于Android中为什么主线程不会因为Looper.loop()里的死循环卡死?引发的思考,事实可能不是一个 epoll 那么 简单。

( 转载请务必标明出处:https://cloud.tencent.com/developer/user/1148436/activities) 前序 本文将...

2365
来自专栏Android 开发学习

IjkPlayer 源码阅读一 初始化

2062
来自专栏不会写文章的程序员不是好厨师

使用BeanFactoryPostProcessor——这种姿势不要用

在公司内,Spring基本都是首选的IOC框架,Spring也提供了很多扩展点让我们介入到容器的生命周期中,例如BeanFactoryPostProcessor...

813
来自专栏androidBlog

使用ViewDragHelper打造属于自己的DragLayout(抽屉开关 )

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/gdutxiaoxu/article/details...

452
来自专栏何俊林

RxJava 在Android中的应用

RxJava 是什么, 能解决什么问题 对比其他技术理解为什么要选择RxJava RxJava的相关API RxJava在Android中的典型使用场景 RxJ...

2007
来自专栏DOTNET

表现层设计模式

一、理论 1 MVC:模型-视图-控制器 模型: 指应用程序中,业务逻辑入口点对象。模型中包括:应用程序状态、视图展示的数据、响应用户请求的操作、执行控制器请求...

2544
来自专栏个人分享

Hive metastore整体代码分析及详解

  从上一篇对Hive metastore表结构的简要分析中,我再根据数据设计的实体对象,再进行整个代码结构的总结。那么我们先打开metadata的目录,其目录...

843
来自专栏向治洪

android仿iphone的地区选择

最近项目要做一个,类似淘宝手机客户端的,选择收货地址的三级联动滚动选择组件,下面是它的大致界面截图: ? 在IOS中有个叫UIPickerView的选择器,...

2977
来自专栏Android中高级开发

Android开发之漫漫长途 XI——从I到X的小结

该文章是一个系列文章,是本人在Android开发的漫漫长途上的一点感想和记录,我会尽量按照先易后难的顺序进行编写该系列。该系列引用了《Android开发艺术探索...

792
来自专栏Spark生态圈

[spark] TaskScheduler 任务提交与调度源码解析

在DAGScheduler划分为Stage并以TaskSet的形式提交给TaskScheduler后,再由TaskScheduler通过TaskSetMagag...

913

扫描关注云+社区