Akka源码分析

Stella981
• 阅读 766

  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实现了某些特殊功能,虽然这点我不太喜欢。

点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
4个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
Karen110 Karen110
3年前
​一篇文章总结一下Python库中关于时间的常见操作
前言本次来总结一下关于Python时间的相关操作,有一个有趣的问题。如果你的业务用不到时间相关的操作,你的业务基本上会一直用不到。但是如果你的业务一旦用到了时间操作,你就会发现,淦,到处都是时间操作。。。所以思来想去,还是总结一下吧,本次会采用类型注解方式。time包importtime时间戳从1970年1月1日00:00:00标准时区诞生到现在
Wesley13 Wesley13
3年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
3年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
3年前
ES6 新增的数组的方法
给定一个数组letlist\//wu:武力zhi:智力{id:1,name:'张飞',wu:97,zhi:10},{id:2,name:'诸葛亮',wu:55,zhi:99},{id:3,name:'赵云',wu:97,zhi:66},{id:4,na
Wesley13 Wesley13
3年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
10个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这