akka中的EventBus其实是不常用,也最容易被忽略的一个组件。

  但如果你深入Cluster的实现就会发现,这个东西其实还挺有用的,而且它是ActorSystem系统中所有事件消息的一个横切面,通过它你可以订阅特定类型的消息,然后做出相应的动作。那读者可能会问了,这个订阅消息也很简单的啊,我自己实现不就好了。嗯,其实你这个想法是对的,akka所有的功能都是基于actor和Actor模型的,所有复杂的功能实现起来都不是特别麻烦,至少实现的模型不会很复杂。不过你可能用不好这个EventBus,因为你并不一定会用,或者说不知道什么时候用。

  对于Event Bus,也就是事件总线,普通场景下个人建议不要使用。Event Bus会使本来就复杂的消息通信更加复杂, 如果不用,开发过程中你明确知道跟某个actor通信的都有哪些actor,也就是说他们之间的通信协议是明确的。仅仅做到这一点,就会使actor系统很复杂了,再用个Event Bus把事件发送出去,会导致消息更加分散,某种意义上也是一种耦合。比如你把消息A发布出去,但却不知道谁在订阅它,如果某个版本升级你不消息忘了发布这个消息,那其他actor还能正常工作吗?这明显是给自己找麻烦。

  那什么时候用呢?或者说使用的时候都有哪些限制呢?大概有两种情况吧:1.发布的都是系统消息,跟业务无关;2.为了考虑系统后期的扩展和升级(当然了需要满足第一个条件)。第一个规则是啥意思呢?就是你发布的消息不会变化或者不会有大的变化,比如只是发布了某个特定actor启动、停止、退出的系统消息,这些消息无论格式还是内容都是固定的。如果后期系统功能升级,需要监控这些消息,由于消息固定,所以不会给版本带来很大的问题。再加上不是业务消息,所以也不会给业务造成什么影响。

  废话不多说,来看看它的实现。当然EventBus实现比较复杂,简单起见,我们只分析Event Stream。

// this provides basic logging (to stdout) until .start() is called below
val eventStream = new EventStream(this, DebugEventStream)
eventStream.startStdoutLogger(settings)

  在ActorSystemImpl中有上面两行代码,创建了一个eventStream,官方文档说,提供了一个基本的日志功能。其实这句话我觉得不应该说,容易给大家造成误解。大家肯定想,既然这个是用来做日志的,就没啥用了呗。如果有这个认识的话,再对akka做扩展的时候会走很大的弯路。其实akka系统通过eventStream发布了很多重要的系统消息,比如actor生命周期状态、remote模式下网络生命周期事件,如果能够合理的使用好这些系统消息,会给我们带来极大的方便,偷偷的告诉你,cluster就是订阅了一些网络状态事件实现了许多重要的功能。

/**
* An Akka EventStream is a pub-sub stream of events both system and user generated,
* where subscribers are ActorRefs and the channels are Classes and Events are any java.lang.Object.
* EventStreams employ SubchannelClassification, which means that if you listen to a Class,
* you'll receive any message that is of that type or a subtype.
*
* The debug flag in the constructor toggles if operations on this EventStream should also be published
* as Debug-Events
*/
class EventStream(sys: ActorSystem, private val debug: Boolean) extends LoggingBus with SubchannelClassification

  Akka EventStream是一个发布-订阅事件流,包括系统和用户产生的数据。订阅某个特定类型的消息,不一定会收到对应的消息,前提是你自己或系统调用EventStream的发布接口把消息发布了出去。

/**
* Classification which respects relationships between channels: subscribing
* to one channel automatically and idempotently subscribes to all sub-channels.
*/
trait SubchannelClassification { this: EventBus ⇒

  SubchannelClassification,子频道分类器,根据官方描述大概知道,它会自动的订阅所有子频道的消息。大概是会自动订阅某个父类所有子类的消息吧。频道是啥?当然是一个类或者接口了啊。

  LoggingBus具体做啥的就不分析了,反正是跟记日志有关的。不过从它的继承关系来看,它直接决定了EventStream是一个EventBus的某个子类。这个继承关系我觉得官方实现的不够合理,毕竟记日志只是EventStream一个功能。EventStream首先应该是一个EventBus,只不过混入了Logging的功能而已,现在直接继承LoggingBus从而继承EventBus,显得不够优化!

class DeadLetterListener extends Actor {
def receive = {
case d: DeadLetter ⇒ println(d)
}
} val listener = system.actorOf(Props[DeadLetterListener])
system.eventStream.subscribe(listener, classOf[DeadLetter])

  这是官方的一个例子,非常简单,就是调用subscribe方法,订阅了DeadLetter类型的消息,把消息发送给DeadLetterListener这个actor。那么来看看subscribe如何实现,不过在这之前还需要看看它是如何初始化的。在ActorSystem的start方法中调用了eventStream.startUnsubscriber(),对eventStream实现了初始化。

  /**
* ''Must'' be called after actor system is "ready".
* Starts system actor that takes care of unsubscribing subscribers that have terminated.
*/
def startUnsubscriber(): Unit =
// sys may be null for backwards compatibility reasons
if (sys ne null) EventStreamUnsubscriber.start(sys, this)

  其中sys就是我们传入的ActorSystem实例。

/**
* INTERNAL API
*
* Provides factory for [[akka.event.EventStreamUnsubscriber]] actors with **unique names**.
* This is needed if someone spins up more [[EventStream]]s using the same [[akka.actor.ActorSystem]],
* each stream gets it's own unsubscriber.
*/
private[akka] object EventStreamUnsubscriber { private val unsubscribersCount = new AtomicInteger(0) final case class Register(actor: ActorRef) final case class UnregisterIfNoMoreSubscribedChannels(actor: ActorRef) private def props(eventStream: EventStream, debug: Boolean) =
Props(classOf[EventStreamUnsubscriber], eventStream, debug) def start(system: ActorSystem, stream: EventStream) = {
val debug = system.settings.config.getBoolean("akka.actor.debug.event-stream")
system.asInstanceOf[ExtendedActorSystem]
.systemActorOf(props(stream, debug), "eventStreamUnsubscriber-" + unsubscribersCount.incrementAndGet())
} }

  官方说EventStreamUnsubscriber是个工厂类,用来给EventStreamUnsubscriber提供一个唯一的名字,如果开发者启动了多个EventStream不至于会出现冲突。其实吧,个人觉得完全没必要,多创建一个EventStream,这都属于高级用法了,akka还没普及,远到不了这个地步。

/**
* INTERNAL API
*
* Watches all actors which subscribe on the given eventStream, and unsubscribes them from it when they are Terminated.
*
* Assumptions note:
* We do not guarantee happens-before in the EventStream when 2 threads subscribe(a) / unsubscribe(a) on the same actor,
* thus the messages sent to this actor may appear to be reordered - this is fine, because the worst-case is starting to
* needlessly watch the actor which will not cause trouble for the stream. This is a trade-off between slowing down
* subscribe calls * because of the need of linearizing the history message sequence and the possibility of sometimes
* watching a few actors too much - we opt for the 2nd choice here.
*/
protected[akka] class EventStreamUnsubscriber(eventStream: EventStream, debug: Boolean = false) extends Actor

  从官方注释来看,EventStreamUnsubscriber是所有订阅eventStream的监督者,当订阅者(也就是某个actor)stop的时候,把对应的订阅消息移除,以便发送不必要的消息。那EventStreamUnsubscriber和EventStream的关系是怎么样的呢?其实吧,这里又做了一个分层,EventStreamUnsubscriber负责监控对应的actor,把消息发送个它,而EventStream负责订阅相关的状态维护。

  初始化完成后,下面来看subscribe的实现。

override def subscribe(subscriber: ActorRef, channel: Class[_]): Boolean = {
if (subscriber eq null) throw new IllegalArgumentException("subscriber is null")
if (debug) publish(Logging.Debug(simpleName(this), this.getClass, "subscribing " + subscriber + " to channel " + channel))
registerWithUnsubscriber(subscriber)
super.subscribe(subscriber, channel)
}
@tailrec
private def registerWithUnsubscriber(subscriber: ActorRef): Unit = {
// sys may be null for backwards compatibility reasons
if (sys ne null) initiallySubscribedOrUnsubscriber.get match {
case value @ Left(subscribers) ⇒
if (!initiallySubscribedOrUnsubscriber.compareAndSet(value, Left(subscribers + subscriber)))
registerWithUnsubscriber(subscriber) case Right(unsubscriber) ⇒
unsubscriber ! EventStreamUnsubscriber.Register(subscriber)
}
}
/** Either the list of subscribed actors, or a ref to an [[akka.event.EventStreamUnsubscriber]] */
private val initiallySubscribedOrUnsubscriber = new AtomicReference[Either[Set[ActorRef], ActorRef]](Left(Set.empty))

  initiallySubscribedOrUnsubscriber的定义还是很奇怪的,不过根据上下文来分析,registerWithUnsubscriber应该就是给EventStreamUnsubscriber发送EventStreamUnsubscriber.Register(subscriber)消息,然后调用super.subscribe

def subscribe(subscriber: Subscriber, to: Classifier): Boolean = subscriptions.synchronized {
val diff = subscriptions.addValue(to, subscriber)
addToCache(diff)
diff.nonEmpty
}

  super.subscribe是在SubchannelClassification中实现的。

  // must be lazy to avoid initialization order problem with subclassification
private lazy val subscriptions = new SubclassifiedIndex[Classifier, Subscriber]()

  第一行的addVelue,应该就是把类型和对应的Subscriber做索引,当然了同一个Classifier是可以有多个订阅者的。Subscriber是啥?当然是一个ActorRef了。这个在EventStream继承的ActorEventBus中定义。

@volatile
private var cache = Map.empty[Classifier, Set[Subscriber]]

  cache其实就是一个map,保存类型与订阅者集合的映射。逻辑是不是也很清晰呢?简单来说,订阅某个消息,就是把消息的类型和对应的actorRef做一个绑定,然后在某个对应类型的消息产生时,调用actorRef的tell函数就行了。

def publish(event: Event): Unit = {
val c = classify(event)
val recv =
if (cache contains c) cache(c) // c will never be removed from cache
else subscriptions.synchronized {
if (cache contains c) cache(c)
else {
addToCache(subscriptions.addKey(c))
cache(c)
}
}
recv foreach (publish(event, _))
}

  那我们来看看publish的具体实现,EventStream中定义了Event就是一个AnyRef,其实就是可以发布任意引用类型的消息。这段代码也比较容易理解,在分析classify之前可以猜一猜,其实就是找出传入的AnyRef具体类型,然后从cache中找到对应的订阅者,在调用publish发布消息。

  protected def classify(event: AnyRef): Class[_] = event.getClass

  EventStream重写了classify函数,很简单,就是getClass。

protected def publish(event: AnyRef, subscriber: ActorRef) = {
if (sys == null && subscriber.isTerminated) unsubscribe(subscriber)
else subscriber ! event
}

  publish呢?就是调用subscriber的! 方法,把消息发送出去。

  其实分析到这里,基本就结束了,特别简单。订阅消息就是把对应的类型和actor关联起来,publish的时候通过消息的类型找到对应的订阅者(也就是actor),把消息发给订阅者就结束了,自己实现也特别简单。不过为了通用和稳定,akka还是做了很多工作的。比如某个actor被Terminat的时候,可以自动取消订阅,毕竟actor还可能意外终止,没有来得及调用unsubscribe方法取消订阅。

  EventStream就分析到这里了,不过介绍这个知识点有两个出发点。首先这个EventStream作为所有消息的截面,特殊情况下,还是很有用的。另外就是在分析cluster的时候,这个点还是比较重要的,毕竟cluster用eventStream实现了某些特殊功能,虽然这点我不太喜欢。

Akka源码分析-Event Bus的更多相关文章

  1. Akka源码分析-Cluster-Metrics

    一个应用软件维护的后期一定是要做监控,akka也不例外,它提供了集群模式下的度量扩展插件. 其实如果读者读过前面的系列文章的话,应该是能够自己写一个这样的监控工具的.简单来说就是创建一个actor,它 ...

  2. Akka源码分析-Cluster-Singleton

    akka Cluster基本实现原理已经分析过,其实它就是在remote基础上添加了gossip协议,同步各个节点信息,使集群内各节点能够识别.在Cluster中可能会有一个特殊的节点,叫做单例节点. ...

  3. Akka源码分析-Persistence

    在学习akka过程中,我们了解了它的监督机制,会发现actor非常可靠,可以自动的恢复.但akka框架只会简单的创建新的actor,然后调用对应的生命周期函数,如果actor有状态需要回复,我们需要h ...

  4. Akka源码分析-Cluster-ActorSystem

    前面几篇博客,我们依次介绍了local和remote的一些内容,其实再分析cluster就会简单很多,后面关于cluster的源码分析,能够省略的地方,就不再贴源码而是一句话带过了,如果有不理解的地方 ...

  5. Akka源码分析-Akka Typed

    对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...

  6. Akka源码分析-Akka-Streams-概念入门

    今天我们来讲解akka-streams,这应该算akka框架下实现的一个很高级的工具.之前在学习akka streams的时候,我是觉得云里雾里的,感觉非常复杂,而且又难学,不过随着对akka源码的深 ...

  7. Akka源码分析-Cluster-Distributed Publish Subscribe in Cluster

    在ClusterClient源码分析中,我们知道,他是依托于“Distributed Publish Subscribe in Cluster”来实现消息的转发的,那本文就来分析一下Pub/Sub是如 ...

  8. Akka源码分析-local-DeathWatch

    生命周期监控,也就是死亡监控,是akka编程中常用的机制.比如我们有了某个actor的ActorRef之后,希望在该actor死亡之后收到响应的消息,此时我们就可以使用watch函数达到这一目的. c ...

  9. Zepto源码分析-event模块

    源码注释 // Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT l ...

随机推荐

  1. <MySQL>入门三 数据定义语言 DDL

    -- DDL 数据定义语言 /* 库和表的管理 一.库的管理:创建.修改.删除 二.表的管理:创建.修改.删除 创建:create 修改:alter 删除:drop */ 1.库的管理 -- 库的管理 ...

  2. FileWriter实现从一个文件中读取内容并写到另一个文件中

    FileWriter和FileOutputStream都是向文件写内容,区别是前台一次写一个字符,后者一次写一个字节 package com.janson.day20180827; import ja ...

  3. eclipse 中常用快捷键

    * 字母大小写转换 ctrl+shift+x   转为大写 ctrl+shift+y   转为小写 * eclipse 自动生成对象来接收方法的返回值的快捷键 说明:光标一定要定位到要自动生成返回值对 ...

  4. VNC 安装 (适用Redhat 9.0 和 CentOS 7.0+)

    Remote Service 本文转自https://www.cnblogs.com/yjscloud/p/6695388.html VNC 安装 (适用Redhat 9.0 和 CentOS 7.0 ...

  5. Linux配置网卡、网卡会话、网卡bonding

    配置网卡  1.路径:  /etc/sysconfig/network-scripts/ifcfg-eno16777728 2.含义:HWADDR=00:0C:29:9C:D6:4D   Mac地址 ...

  6. 利用stylist插件,简单两步屏蔽新浪微博上的广告

    以前新浪微博只是在侧栏有几块小小的广告,还算可以接受,想着忍忍就算了,可最近真是越来越不厚道了,自从和淘宝合作之后,侧栏就开始有一大块广告根据你在淘宝的搜索记录推荐商品,更可恶的是信息流里的祛痘微博现 ...

  7. python爬虫26 | 把数据爬取下来之后就存储到你的MySQL数据库。

    小帅b说过 在这几篇中会着重说说将爬取下来的数据进行存储 上次我们说了一种 csv 的存储方式 这次主要来说说怎么将爬取下来的数据保存到 MySQL 数据库 接下来就是 学习python的正确姿势 真 ...

  8. CSC

    CSC CSC Table of Contents 1. account 2. Contacts 3. <国家公派留学人员预订回国机票说明> 4. 回国手续 4.1. 申办及开具<留 ...

  9. 有一张表里面有上百万的数据,在做查询的时候,如何优化?从数据库端,java端和查询语句上回答

    原文:https://www.2cto.com/database/201612/580140.html 1)数据库设计方面: a. 对查询进行优化,应尽量避免全表扫描,首先应考虑在 where 及 o ...

  10. sdibt 1244类似于拓扑排序

    博客:http://blog.csdn.net/mypsq/article/details/39005991 #include<stdio.h> #include<string.h& ...