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。

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

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

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

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

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

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

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

  1. class DeadLetterListener extends Actor {
  2. def receive = {
  3. case d: DeadLetter println(d)
  4. }
  5. }
  6.  
  7. val listener = system.actorOf(Props[DeadLetterListener])
  8. system.eventStream.subscribe(listener, classOf[DeadLetter])

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

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

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

  1. /**
  2. * INTERNAL API
  3. *
  4. * Provides factory for [[akka.event.EventStreamUnsubscriber]] actors with **unique names**.
  5. * This is needed if someone spins up more [[EventStream]]s using the same [[akka.actor.ActorSystem]],
  6. * each stream gets it's own unsubscriber.
  7. */
  8. private[akka] object EventStreamUnsubscriber {
  9.  
  10. private val unsubscribersCount = new AtomicInteger(0)
  11.  
  12. final case class Register(actor: ActorRef)
  13.  
  14. final case class UnregisterIfNoMoreSubscribedChannels(actor: ActorRef)
  15.  
  16. private def props(eventStream: EventStream, debug: Boolean) =
  17. Props(classOf[EventStreamUnsubscriber], eventStream, debug)
  18.  
  19. def start(system: ActorSystem, stream: EventStream) = {
  20. val debug = system.settings.config.getBoolean("akka.actor.debug.event-stream")
  21. system.asInstanceOf[ExtendedActorSystem]
  22. .systemActorOf(props(stream, debug), "eventStreamUnsubscriber-" + unsubscribersCount.incrementAndGet())
  23. }
  24.  
  25. }

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

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

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

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

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

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

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

  super.subscribe是在SubchannelClassification中实现的。

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

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

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

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

  1. def publish(event: Event): Unit = {
  2. val c = classify(event)
  3. val recv =
  4. if (cache contains c) cache(c) // c will never be removed from cache
  5. else subscriptions.synchronized {
  6. if (cache contains c) cache(c)
  7. else {
  8. addToCache(subscriptions.addKey(c))
  9. cache(c)
  10. }
  11. }
  12. recv foreach (publish(event, _))
  13. }

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

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

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

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

  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. 九度oj 1179 阶乘

    题目1179:阶乘 时间限制:1 秒 内存限制:32 兆 特殊判题:否 提交:6010 解决:1756 题目描述: 输入n,求y1=1!+3!+...m!(m是小于等于n的最大奇数)y2=2!+4!+ ...

  2. 添物零基础到大型全栈架构师 不花钱学计算机及编程(预备篇)— C语言编程基础

    ​C语言介绍 C语言基本是每个编程人员必学的一面语言,很好掌握,是理解编程的关键.很多编程语言基于其编写或者基于此语言的衍生品编写. C语言是人机交互的一个基础语言之一,虽然是之一,单一般其实就是唯一 ...

  3. Spark在Executor上的内存分配

    spark.serializer (default org.apache.spark.serializer.JavaSerializer )    建议设置为 org.apache.spark.ser ...

  4. [luoguP1282] 多米诺骨牌(DP + 背包)

    传送门 将问题转换成分组背包,每一组有上下两个,每一组中必须选则一个,上面的价值为0,下面的价值为1,求价值最小 因为要求上下两部分差值最小,只需从背包大小为总数 / 2 时往前枚举,找最小答案即可. ...

  5. [COGS309] [USACO 3.2] 香甜的黄油

    ★★   输入文件:butter.in   输出文件:butter.out   简单对比 时间限制:1 s   内存限制:128 MB 描述 农夫John发现做出全威斯康辛州最甜的黄油的方法:糖.把糖 ...

  6. POJ 1328 Radar Installation 贪心算法

    Description Assume the coasting is an infinite straight line. Land is in one side of coasting, sea i ...

  7. The Evaluation of Determinant(求行列式mod一个数的值)

    #include<cstdio> #include<iostream> #include<algorithm> #include<cstring> #i ...

  8. T1002 搭桥 codevs

    http://codevs.cn/problem/1002/  时间限制: 1 s  空间限制: 128000 KB  题目等级 : 黄金 Gold 题目描述 Description 有一矩形区域的城 ...

  9. mybatis mapper文件sql语句传入hashmap参数

    1.怎样在mybatis mapper文件sql语句传入hashmap参数? 答:直接这样写map就可以 <select id="selectTeacher" paramet ...

  10. php 类型转换

    PHP数据类型转换 PHP的数据类型转换属于强制转换,允许转换的PHP数据类型有: •(int).(integer):转换成整形 •(float).(double).(real):转换成浮点型 •(s ...