Akka源码分析-Event Bus
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的更多相关文章
- Akka源码分析-Cluster-Metrics
一个应用软件维护的后期一定是要做监控,akka也不例外,它提供了集群模式下的度量扩展插件. 其实如果读者读过前面的系列文章的话,应该是能够自己写一个这样的监控工具的.简单来说就是创建一个actor,它 ...
- Akka源码分析-Cluster-Singleton
akka Cluster基本实现原理已经分析过,其实它就是在remote基础上添加了gossip协议,同步各个节点信息,使集群内各节点能够识别.在Cluster中可能会有一个特殊的节点,叫做单例节点. ...
- Akka源码分析-Persistence
在学习akka过程中,我们了解了它的监督机制,会发现actor非常可靠,可以自动的恢复.但akka框架只会简单的创建新的actor,然后调用对应的生命周期函数,如果actor有状态需要回复,我们需要h ...
- Akka源码分析-Cluster-ActorSystem
前面几篇博客,我们依次介绍了local和remote的一些内容,其实再分析cluster就会简单很多,后面关于cluster的源码分析,能够省略的地方,就不再贴源码而是一句话带过了,如果有不理解的地方 ...
- Akka源码分析-Akka Typed
对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...
- Akka源码分析-Akka-Streams-概念入门
今天我们来讲解akka-streams,这应该算akka框架下实现的一个很高级的工具.之前在学习akka streams的时候,我是觉得云里雾里的,感觉非常复杂,而且又难学,不过随着对akka源码的深 ...
- Akka源码分析-Cluster-Distributed Publish Subscribe in Cluster
在ClusterClient源码分析中,我们知道,他是依托于“Distributed Publish Subscribe in Cluster”来实现消息的转发的,那本文就来分析一下Pub/Sub是如 ...
- Akka源码分析-local-DeathWatch
生命周期监控,也就是死亡监控,是akka编程中常用的机制.比如我们有了某个actor的ActorRef之后,希望在该actor死亡之后收到响应的消息,此时我们就可以使用watch函数达到这一目的. c ...
- Zepto源码分析-event模块
源码注释 // Zepto.js // (c) 2010-2015 Thomas Fuchs // Zepto.js may be freely distributed under the MIT l ...
随机推荐
- 九度oj 1179 阶乘
题目1179:阶乘 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:6010 解决:1756 题目描述: 输入n,求y1=1!+3!+...m!(m是小于等于n的最大奇数)y2=2!+4!+ ...
- 添物零基础到大型全栈架构师 不花钱学计算机及编程(预备篇)— C语言编程基础
C语言介绍 C语言基本是每个编程人员必学的一面语言,很好掌握,是理解编程的关键.很多编程语言基于其编写或者基于此语言的衍生品编写. C语言是人机交互的一个基础语言之一,虽然是之一,单一般其实就是唯一 ...
- Spark在Executor上的内存分配
spark.serializer (default org.apache.spark.serializer.JavaSerializer ) 建议设置为 org.apache.spark.ser ...
- [luoguP1282] 多米诺骨牌(DP + 背包)
传送门 将问题转换成分组背包,每一组有上下两个,每一组中必须选则一个,上面的价值为0,下面的价值为1,求价值最小 因为要求上下两部分差值最小,只需从背包大小为总数 / 2 时往前枚举,找最小答案即可. ...
- [COGS309] [USACO 3.2] 香甜的黄油
★★ 输入文件:butter.in 输出文件:butter.out 简单对比 时间限制:1 s 内存限制:128 MB 描述 农夫John发现做出全威斯康辛州最甜的黄油的方法:糖.把糖 ...
- POJ 1328 Radar Installation 贪心算法
Description Assume the coasting is an infinite straight line. Land is in one side of coasting, sea i ...
- The Evaluation of Determinant(求行列式mod一个数的值)
#include<cstdio> #include<iostream> #include<algorithm> #include<cstring> #i ...
- T1002 搭桥 codevs
http://codevs.cn/problem/1002/ 时间限制: 1 s 空间限制: 128000 KB 题目等级 : 黄金 Gold 题目描述 Description 有一矩形区域的城 ...
- mybatis mapper文件sql语句传入hashmap参数
1.怎样在mybatis mapper文件sql语句传入hashmap参数? 答:直接这样写map就可以 <select id="selectTeacher" paramet ...
- php 类型转换
PHP数据类型转换 PHP的数据类型转换属于强制转换,允许转换的PHP数据类型有: •(int).(integer):转换成整形 •(float).(double).(real):转换成浮点型 •(s ...