Akka实战——快速上手

本章内容

  • 定义一个 Actor 及其行为
  • 实例化 Actor 并向其发送消息
  • 用变量在 Actor 内保存状态
  • 用行为在 Actor 内保存状态
  • 按计划(定时)发送消息

本章将把上一章的 Akka 基础落到实处:创建 Actor、向 Actor 发送消息、设置 Actor 的行为;还会讲如何处理状态,以及如何按计划延迟发送消息。

我们从一个最小的 Akka 应用开始:一个可以往里"存钱"的钱包(wallet)。它可以作为更大"体育赛事投注"应用的钱包子系统。你会在本书后面学习如何实现这个投注应用,但首先要把基础打牢。

注意 要运行本书示例,请按附录 A 的说明安装所有工具。安装完成后再回到这里开始。

本章会实现几种"钱包"变体,借此串讲一些 Akka 原语:

  • 最开始,钱包仅把存入的钱打印到控制台,不维护总额。
  • 随后,你将学习如何把金额存进变量。
  • 最后,你会实现钱包的"停用/启用"。

注意 本章源代码见 www.manning.com/books/akka-...
github.com/franciscolo...。任意代码片段/清单都能在同名.scala 文件里找到,例如 object Wallet 的片段在 Wallet.scala 中。

2.1 打印金额

本节你将创建一个应用和一个 Actor,并向该 Actor 发送两条消息,随后它会把消息输出到控制台。示例如下:Wallet$ 是 Actor 的类名,后面是 Actor 的日志------每发送一条消息就一行(回忆:Actor 的行为像一个消息队列):

scss 复制代码
[Wallet$] - received '1' dollar(s)
[Wallet$] - received '10' dollar(s)

接下来直接看组成该应用的 Actor 与主类。

2.2 开始编码

创建 Actor 前,必须先声明它能接收的消息类型。不能"来者不拒",你需要定义 Actor 的协议(protocol)。

2.2.1 Actor 的协议

Akka API 通过 Behavior[Int] 指定 Wallet Actor 的行为(只能接收 Int 型消息)。这就是它的协议。本例中,Wallet Actor 的协议类型为 Int,定义如下:

arduino 复制代码
import akka.actor.typed.Behavior

object Wallet {

  def apply(): Behavior[Int] = ...
}

此时 Actor 的其余代码并不重要。实现可以很多样,但接下来开发的关键在于:明确该 Actor 能接收哪些消息

2.2.2 创建应用并实例化 Actor

协议确定后,就可以通过上一章讲到的 ActorSystem 创建应用并添加 Actor。应用就是一个可执行程序。你可以定义一个继承自 scala.App 的对象来创建它:

scala 复制代码
object WalletApp extends App

这是一种创建可运行类的简单方式。最后会讲如何运行。

接下来关注 ActorSystem 构造时的签名,尤其是第一个参数:

scala 复制代码
object WalletApp extends App {

  val guardian: ActorSystem[Int] = ActorSystem(Wallet(), "wallet")    ❶

}

❶ 实例化一个 Akka 应用

在 Scala 中,泛型(等同于 Java 的泛型)用方括号标注。下面是 ActorSystemapply(构造)方法,其泛型参数用 [T] 表示:

less 复制代码
def apply[T](guardianBehavior: Behavior[T], name: String): ActorSystem[T]

注意 在 Scala 中,像 ActorSystem 这样的对象通过 apply 创建,也可以仅写对象名调用。比如 ActorSystem(myParameters) 实际调用的是 ActorSystem.apply(myParameters)。同理,Wallet() 等价于 Wallet.apply()

所以,ActorSystem(Wallet(), "wallet") 的第一个参数就是 Wallet 的构造(apply)。

有了 Actor,下面开始发消息。

2.2.3 发送消息

这里有两点要注意。其一,Wallet Actor 被实例化成 ActorSystem,且类型为 [Int],表明它只能 接收 Int 消息。其二,上一章提到过,ActorSystem 是一个指向 Actor 的引用,即 ActorRef------你可以用它给 Actor 发消息。发送使用 tell 方法,通常用"感叹号"操作符 ! 表示。比如:

复制代码
myActorReference ! 1

把这些拼在一起,下面的代码定义并实例化了 Actor,并发送两条消息:

scala 复制代码
import akka.actor.typed.ActorSystem

object WalletApp extends App {

  val guardian: ActorSystem[Int] = ActorSystem(Wallet(), "wallet")  ❶
  guardian ! 1                                                      ❷
  guardian ! 10                                                     ❷
}

❶ 创建 Actor

❷ 向 Actor 发送消息

首先发送整数 1,然后发送整数 10。Actor 的类型是 IntActorSystem[Int] 中已显式声明),从代码能成功编译也可以验证------如果你给不接收 Int 的 Actor 发送 Int编译就会报错,应用根本运行不起来。这个类型检查(尤其是在重构时改变了 Actor 协议,而旧协议仍在某处使用时由编译器提示)是 Akka Typed 相比旧版本的巨大提升。接下来看看 Actor 如何接收消息。

2.2.4 实现 Actor:接收消息

如前所述,Actor 的完整类型是 Behavior[Int]。实现行为要用行为工厂(behavior factories)。Behaviors.receive 是一个工厂函数,输入是 contextmessage(分别是 ActorContext 与收到的 Int),输出是一个 Behavior。其签名如下:

csharp 复制代码
object Wallet {

  def apply(): Behavior[Int] =
    Behaviors.receive { (context, message) =>    ❶
      ...
    }
}

Behaviors.receive 的签名

在工厂中可以使用 messagecontext,例如用 context 的日志功能打日志:

csharp 复制代码
object Wallet {

  def apply(): Behavior[Int] =
    Behaviors.receive { (context, message) =>
      context.log.info(s"received '$message' dollar(s)")    ❶
      ...
    }
}

❶ 记录每条收到的消息

最后,需要为下一条 消息指定行为。这里可以继续使用同一个行为,对应工厂是 Behaviors.same。合在一起,Actor 的最终实现如下:

csharp 复制代码
object Wallet {

  def apply(): Behavior[Int] =
    Behaviors.receive { (context, message) =>
      context.log.info(s"received '$message' dollar(s)")
      Behaviors.same                                      ❶
    }
}

❶ 下一条消息仍用相同的行为处理

注意这些 Behaviors 像"套娃"一样嵌套:外层 def apply(): Behavior[Int] 包含一个 Behaviors.receive,其内部又返回 Behaviors.same。你会反复看到这种模式。

2.2.5 终止系统

应当优雅地 终止 ActorSystem,以便系统能关闭持有的模块资源(比如 HTTP、数据库连接等)。做法是对 guardian(ActorSystem)调用 .terminate()

因为示例在 sbt shell 中运行,你可以加个钩子来触发终止,比如监听一次回车:使用 scala.io.StdIn.readLine(),按下 Enter 即触发:

scala 复制代码
object WalletApp extends App {
  ...
  println("Press ENTER to terminate")
  scala.io.StdIn.readLine()
  guardian.terminate()
}

程序会在 .readLine() 处阻塞等待。你按回车后,继续执行终止系统。

注意 sbt shell 是用来编译/运行 sbt 项目的交互式 shell。安装 sbt 后,在命令行输入 sbt 即可进入。在其中可以编译、构建、测试并运行 Scala/Java/C/C++ 程序。

2.2.6 应用程序

结合前面的内容,完整代码如下。

代码清单 2.1 钱包完整代码

scala 复制代码
object WalletApp extends App {

  val guardian: ActorSystem[Int] = ActorSystem(Wallet(), "wallet")   ❶
  guardian ! 1                                                       ❷
  guardian ! 10                                                      ❷

  println("Press ENTER to terminate")
  scala.io.StdIn.readLine()
  guardian.terminate()
}

object Wallet {

  def apply(): Behavior.Receive[Int] =                               ❸
    Behaviors.receive { (context, message) =>
      context.log.info(s"received '$message' dollar(s)")
      Behaviors.same
    }
}

❶ 实例化一个 Actor

❷ 向 Actor 发送 Int 类型消息

❸ 定义 Wallet Actor 如何响应每条消息的行为

2.2.7 Git 中的解决方案

该钱包实现在 akka-topics 仓库中。克隆项目后,在根目录可以看到 build.sbtsbt 用它来编译、运行和测试。项目包含多个子项目(每章一个),便于你边读边跑、边测、边改。每个子项目包含:

  • 名称 ------用一个 val 定义
  • 文件位置 ------相对于 build.sbt 的子项目文件夹名
  • Scala 版本 ------设置后由 sbt 自动下载
  • 库依赖 ------子项目用到的库,同样由 sbt 自动下载

本章所有示例实现于 chapter02 子项目,其在 build.sbt 中的定义如下:

less 复制代码
val chapter02 = project
  .in(file("chapter02"))                                       ❶
  .settings(
    scalaVersion := "2.13.1",                                  ❷
    libraryDependencies ++= Seq(
      "com.typesafe.akka" %% "akka-actor-typed" % "2.6.20",    ❸
      "ch.qos.logback" % "logback-classic" % "1.2.3"
      )
    )

...

❶ 子项目所在的文件夹

❷ Scala 版本

❸ Akka Typed 模块

本书围绕 Akka Typed 展开,这里引用的就是其核心模块。

2.2.8 运行应用

要运行钱包应用,打开命令行进入 akka-topics 根目录,启动 sbt shell:

ruby 复制代码
$ sbt

你应看到类似清单 2.2 的输出。大多数内容可以忽略,最重要的是第一行(sbt 与 Java 版本)和最后一行(sbt server started,说明进入了 sbt shell)。

清单 2.2 启动 sbt shell

css 复制代码
[info] welcome to sbt 1.5.0 (AdoptOpenJDK Java 11.0.9)                ❶
[info] loading settings for project global-plugins from build.sbt ...
[info] loading global plugins from /Users/francisco/.sbt/1.0/plugins
[info] loading settings for project code-build from plugins.sbt ...
[info] loading project definition from /Users/francisco/AkkaBook/code/project
[info] loading settings for project code from build.sbt ...
[info] resolving key references (24537 settings) ...
[info] set current project to code (in build file:/Users/francisco/AkkaBook/code/)
[info] sbt server started at local:///Users/francisco/.sbt/1.0/server/347d55a887437ea58247/sock
[info] started sbt server                                             ❷
sbt:code>

❶ sbt 与 Java 版本

❷ 表示 server 已启动

build.sbt 中已经定义了 chapter02 子项目(GitHub: github.com/franciscolo... )。因此在 sbt shell 中可以选中该子项目并运行:

markdown 复制代码
> project chapter02
> run

你的输出应类似如下,会列出当前子项目(chapter02)中可运行的程序,让你选择其一:

清单 2.3 子项目 chapter02 中可运行的应用

less 复制代码
[info] compiling 13 Scala sources to 
➥/Users/francisco/AkkaBook/code/chapter02/target/scala-2.13/classes ...
Multiple main classes detected. Select one to run:
 [1] com.manning.WalletApp
 [2] com.manning.WalletOnOffApp
 [3] com.manning.WalletStateApp
 [4] com.manning.WalletTimerApp

Enter number:

输入 1 选择 WalletApp,即可看到运行时的输出。

2.3 用变量保存状态

接下来你将学习如何给钱包增加状态 ,以便记录钱包中的总金额。状态管理是 Akka 最重要的能力之一。在分布式并发环境里,管理状态是个硬问题;而在 Akka 中更容易一些,因为你是在 Actor 内按顺序、逐条消息地管理状态。我们来看看怎么做。

状态可以由**行为(behavior)**来实现------也就是函数。在这个示例里,钱包把数值存到 total,该值可以在一定范围内增减。为简化起见,最大值设为 2,最小值设为 0(零)。当超过最大/最小值时,钱包就不再增/减,并且 Actor 停止运行,以表示钱包进入了不可接受的状态。

你已经知道什么是**协议(protocol)**了。此前我们用"整数"作为协议类型只是为了演示;真实场景的协议通常由若干类型组成,它们同属一个层级,并以一个 sealed trait 作为顶层。

定义trait 类似于 Java 的接口。关于 trait 参见 docs.scala-lang.org/tour/traits...sealed trait 允许编译器检查你的模式匹配是否穷尽 ;不穷尽会有编译期告警。可参见 "Sealed types" 一节:mng.bz/Pz8v

因此,这个钱包不用 Behavior[Int],而是 Behavior[Command]。它的协议包含两类消息:Increase(amount: Int)Decrease(amount: Int),二者都继承自 Command

scala 复制代码
object WalletState {

  sealed trait Command
  final case class Increase(amount: Int) extends Command
  final case class Decrease(amount: Int) extends Command

  ...
}

你可能会问,为什么把协议类型命名为 Command?这是一种常见命名,便于一眼看出哪些消息是请求 Actor 执行动作的。并非所有消息都是命令,你会在下一章看到例子。

注意 你可能注意到:给 Increase 传一个负数也能"减少"总额。要避免这种情况,可以在读取时统一取绝对值,或在数值非法时直接报错。

钱包的初始值是 0,最大值是 2。这就是为什么清单 2.4 里用工厂方法 WalletState(0, 2)。该 Actor 在创建时接收入参(创建方式见清单之后的说明)。创建好钱包后,就可以按其协议发送消息。为简洁,这里只展示递增。

代码清单 2.4 创建钱包的应用

scala 复制代码
object WalletStateApp extends App {

  val guardian: ActorSystem[WalletState.Command] =
    ActorSystem(WalletState(0, 2), "wallet-state")    ❶
  guardian ! WalletState.Increase                     ❷
  guardian ! WalletState.Increase                     ❷
  guardian ! WalletState.Increase                     ❷

}

❶ 初始总额为 0,最大值为 2

❷ 发送消息

像之前一样,在命令行中运行:

markdown 复制代码
> project chapter02
> run

选择 3,WalletStateApp。你会看到如下输出。钱包会把总额增加到上限,然后"过载":

ruby 复制代码
INFO WalletState$ - increasing to 1
INFO WalletState$ - increasing to 2
INFO WalletState$ - I'm overloaded. Counting '3' while max is '2'. Stopping.

要把"总额"和"最大值"存进 Actor,你需要学点新东西:如何在创建 Actor 时传入初始值。在前一个示例里,我们创建 Actor 时没有传参。

首先,把两个"变量"(初始总额与最大值)加到 Actor 的构造器里------也就是 Walletdef apply 方法。严格说它们不是"变量",在 Scala 里叫(不可变变量)。添加后的签名如下:

less 复制代码
def apply(total: Int, max: Int): Behavior[Command]

其次,必须让这两个入参的值能从一次消息处理 保留到下一次 (即从一次调用保留到下一次调用),从而在第一次递增之后,在下一条递增消息到来时还能拿到更新后的 totalmax。要做到这一点,就不能再用 Behaviors.same 返回同一个行为,而是要直接再次调用 apply(current, max) 并传入更新后的值(钱包还包含更多逻辑,这里只展示维持 totalmax 这两个状态所需的关键部分):

arduino 复制代码
object WalletState {

  def apply(total: Int, max: Int): Behavior[Command] =
    Behavior.receive { (context, message) =>          ❶
      ...
      val current = total + 1                          ❷
      ...
      apply(current, max)                              ❸
    }
}

❶ 输入函数

❷ 递增总额

❸ 指定下一步的行为,同时带上新的 totalmax

因为行为就是函数 ,你可以用它们的输入与输出把状态从一个函数(一次行为)"传递"到下一个。这里,在 def apply(total: Int, max: Int) 的末尾返回 apply(current, max),意味着下一条消息仍由 apply 来处理,但带着更新过的入参。第一次时,totalmax 是通过 .apply(0, 2)(即 WalletState(0, 2),感谢 Scala 的语法糖)设置的;而处理了一条 Increase 后,下一次的行为就是 .apply(1, 2)。通过这种方式,行为获得了新的入参,状态也就从一条消息传递到下一条消息

最后,当达到 max 时,Actor 通过把下一步行为设置为 Behaviors.stopped 来停止。

注意 一旦 Actor 停止,它将无法再处理任何消息。其邮箱中剩余或之后发送到其地址的消息,系统会转发给名为 deadLetters 的 Actor(第 3 章详述)。另外要注意:apply() 不是"递归函数",而是一个工厂 :它接收一个函数作为输入,输出一个 Behavior(一个函数)。定义 apply 的工厂 Behaviors.receive 接收一个 (context, message) => Behavior 类型的函数,并返回一个 Behavior。这是一种函数式风格,不涉及递归。

来看钱包在收到 Increase 时的日志与状态管理。

代码清单 2.5 带状态的钱包 Actor

scala 复制代码
import akka.actor.typed.Behavior
import akka.actor.typed.scaladsl.Behaviors

object WalletState {

  sealed trait Command
  final case class Increase(amount: Int) extends Command
  final case class Decrease(amount: Int) extends Command

  def apply(total: Int, max: Int): Behavior[Command] =   ❶
    Behaviors.receive { (context, message) =>
      message match {
        case Increase(amount) =>
          val current = total + amount
          if (current <= max) {
            context.log.info(s"increasing to $current")
            apply(current, max)                          ❷
          } else {
            context.log.info(
              s"I'm overloaded. Counting '$current' while 
              ➥max is '$max'. Stopping")
            Behaviors.stopped                            ❸
          }
        case Decrease(amount) =>                         ❹
          ...
      }
    }
}

❶ 输入参数 totalmax 共同构成状态

❷ 返回"下一步行为",而不是调用某个方法

❸ 停止 Actor。邮箱里剩余的消息将不会到达该 Actor。

❹ 与 Increase 类似,只是逻辑相反

回忆一下:当钱包递增且当前值未超过最大值时,通过把下一步行为设置为 WalletState.apply(current, max) 来继续增加。从第一条到第二条消息,行为从 .apply(0, 2) 变为 .apply(0 + 1, 2)

反过来,Decrease 在钱包值变成负数时也会停止。代码几乎一样,这里不再重复,感兴趣可在仓库中查看。

注意协议是写在 Actor 内部的。协议消息总是特定于某个 Actor,从这个意义上讲,它们并不打算被其它 Actor 复用。它们就是该 Actor 的 API,理应属于它。

本节你学会了如何把状态作为值 在 Actor 中管理:把状态加到构造器里,并把它传递给"下一步行为"。不过,还有另一种在 Actor 中保存状态的方式:用行为本身来存储状态

2.4 用行为(behaviors)保存状态

在 Actor 中用不同行为 来保存状态,与用变量 保存状态的目的不同。使用行为是为了表达:Actor 在不同状态下对同一类消息会有不同反应。如果你只是想在 Actor 内部改动变量,不要用这种方式;请使用上一节介绍的方法。

根据 Actor 所处的状态,同一条消息可能产生完全不同的效果。比如,一个充当代理(proxy)的 Actor 可以有两个状态:开启(open)关闭(closed) 。开启时,它会把消息转发到另一处;关闭时,它不会转发,而是通知第三方。

我们再实现一个钱包。与前一个变体类似,为简单起见,这里只支持增加 金额,不支持减少。新变化在于:这个钱包有两个状态,对应两个行为:activated(已启用)deactivated(已停用) 。当钱包处于启用状态,收到 Increase 时与前例一样会增加总额;当钱包处于停用状态,总额不可更改,收到 Increase 会丢弃并报告未变更。

注意 这种特性让该 Actor 成为一个有限状态机(FSM) ,因为它对同样的输入会有不同反应。简单说,FSM 具有有限个状态;根据所处状态(也称行为),相同输入可能产生不同结果。Actor 以行为来定义,天然非常适合实现这种模型。

为简化,钱包不再有最大值,也不支持减少;初始总额默认为 0。其协议如下:

scala 复制代码
object WalletOnOff {

  sealed trait Command
  final case object Increase(amount: Int) extends Command
  final case object Activate extends Command
  final case object Deactivate extends Command
  
  ...
}

实例化该钱包无需入参,因为没有最大值且初始金额默认为 0。你可以在停用启用 两种状态下都给它发 Increase。注意默认状态为启用。下面的代码展示了先增加、再停用、再启用、再增加:

scala 复制代码
object WalletOnOffApp extends App {

  val guardian: ActorSystem[WalletResume.Command] =
    ActorSystem(WalletOnOff(), "wallet-on-off")
  guardian ! WalletResume.Increase(1)            ❶
  guardian ! WalletResume.Deactivate             ❷
  guardian ! WalletResume.Increase(1)            ❸
  guardian ! WalletResume.Activate               ❹
  guardian ! WalletResume.Increase(1)            ❺
}

❶ 增加钱包金额

❷ 停用钱包

❸ 钱包处于停用,金额不会增加

❹ 启用钱包

❺ 增加钱包金额

在相同子项目下,于 sbt shell 选择选项 2(WalletOnOffApp)运行该程序,输出如下:

css 复制代码
INFO  [WalletOnOff$] - increasing to 1
INFO  [WalletOnOff$] - wallet is deactivated. Can't increase
INFO  [WalletOnOff$] - activating
INFO  [WalletOnOff$] - increasing to 2

这些功能都可以用你在本章已见过的方式实现。通过多写几个类似 .apply 的方法,你就能拥有多个行为,并在它们之间切换。

要实现"默认总额为 0,默认状态为启用",可以这样写:

css 复制代码
object WalletOnOff {
  
  ...

  def apply(): Behavior[Command] = activated(0)
     activated(0)

  ...
}

**启用(activated)行为在收到 Increment 时增加总额,收到 Deactivate 时切换到 停用(deactivated)**行为。钱包定义了 activateddeactivated 两个行为。先看启用态。

清单 2.6 WalletOnOff 中的 activated 行为

scss 复制代码
object WalletOnOff {
  
  ...   

  def activated(total: Int): Behavior[Command] =
    Behaviors.receive { (context, message) =>
      message match {
        case Increase(amount) =>                     ❶
          val current = total + amount
          context.log.info(s"increasing to $current")
          activated(current)
        case Deactivate =>                           ❷
          deactivated(total)
        case Activate =>                             ❸
          Behaviors.same                             ❸
      }
    }
}

❶ 增加总额并记录日志

❷ 切换到停用行为

❸ 保持相同行为与相同总额

你也许会问:在收到 Activate 时,为何返回 Behavior.same 而不是 activated(total)?因为这里没有发生增加操作,行为完全不变,用 Behavior.same 更直观(当然也可以传回同一个 total,但 Behavior.same 更易读)。

**停用(deactivated)**行为如下:收到 Increase 时只记录"无法增加",收到 Activate 时回到启用态。停用时总额不变。

清单 2.7 deactivated 行为

typescript 复制代码
object WalletOnOff {
  
  ...   

  def deactivated(total: Int): Behavior[Command] = {
    Behaviors.receive { (context, message) =>
      message match {
        case Increase =>                                   ❶
          context.log.info(s"wallet is deactivated. Can't increase")
          Behaviors.same
        case Deactivate =>                                 ❷
          Behaviors.same
        case Activate =>                                   ❸
          context.log.info(s"activating")
          activated(total)
      }
    }
  }
}

❶ 记录无法增加

❷ 保持相同行为与相同总额

❸ 切回启用行为(总额不变)

整个 Actor 代码如下。

清单 2.8 WalletOnOff Actor

scala 复制代码
object WalletOnOff {

  sealed trait Command
  final case class Increase(amount: Int) extends Command
  final case object Deactivate extends Command
  final case object Activate extends Command

  def apply(): Behavior[Command] = activated(0)           ❶

  def activated(total: Int): Behavior[Command] =          ❷
    Behaviors.receive { (context, message) =>
      message match {
        case Increase(amount) =>
          val current = total + amount
          context.log.info(s"increasing to $current")
          resume(current)
        case Deactivate =>
          deactivated(total)
        case Activate =>
          Behaviors.same
      }
    }

  def deactivated(total: Int): Behavior[Command] = {      ❸
    Behaviors.receive { (context, message) =>
      message match {
        case Increase =>
          context.log.info(s"wallet is deactivated. Can't increase")
          Behaviors.same
        case Deactivate =>
          Behaviors.same
        case Activate =>
          context.log.info(s"activating")
          activated(total)
      }
    }
}

❶ 默认行为

❷ 启用态行为

❸ 停用态行为

本节展示了如何在 Akka Typed 中创建一个 FSM。Akka 的原语本身就具备 FSM 语义------你可以说,Actor 模型的公理 就包含了这些语义。你在第 1 章见过这条公理:"Actor 必须指定用于处理下一条消息的行为。"Akka 将这条公理直接落地,因此你只用最基本的构件------行为------就能构建 FSM。

在并发环境里,以"等待时间"的思路来编程极易出错。Akka 简化了这件事:与其让 Actor 主动等待(像线程那样),不如给它排程发送消息

2.5 调度一条消息(Scheduling a message)

我们再实现一个与前一款非常相似的钱包。除了一个差异外,其他一切相同:这个钱包可以接收"停用若干秒"的消息,从而在一段时间内被停用。

有了这种停用方式,钱包的使用者无需记得再发"启用"消息来恢复;钱包在被停用后,会在数秒后给自己发送一条"启用"消息 。具体等待多久由之前收到的停用消息中的持续时间来决定。请注意,这里没有任何阻塞------Actor 端不会"等待",而是像往常一样继续处理消息;调度只是在指定时间点把消息发出。

协议如下所示:

scala 复制代码
object WalletTimer {

  sealed trait Command
  final case class Increase(amount: Int) extends Command
  final case class Deactivate(seconds: Int) extends Command
  private final case object Activate extends Command

  ...
}

与之前不同的是,Deactivate 现在带有持续时间参数;而 Activate 被标记为 private,因此无法从 Actor 外部激活钱包 ,只能由钱包自己激活。该变体命名为 WalletTimer

注意 在 Akka 中,将协议区分为公共私有 是常见模式。这样你既可以提供给其他 Actor 使用的公共 API (一组消息),也能保留只处理 Actor 内部机能的私有 API

使用这个新钱包,可以这样创建应用:

scala 复制代码
object WalletTimerApp extends App {

  val guardian: ActorSystem[WalletTimer.Command] =
    ActorSystem(WalletTimer(), "wallet-timer")
  guardian ! WalletTimer.Increase(1)
  guardian ! WalletTimer.Deactivate(3)

}

在相同子项目的 sbt shell 中选择选项 4(WalletTimerApp)运行,输出如下:

ini 复制代码
10:10:49,896 INFO  [WalletTimer$] - increasing to 1
10:10:52,914 INFO  [WalletTimer$] - activating       ❶

❶ 3 秒后恢复

要实现对 Activate定时发送 ,需要用到工厂 Behaviors.withTimers 创建定时器构建器 ,再用其 .startSingleTimer 启动一次性 定时器。这部分本身就是一个 Behavior,可以像其他行为(activateddeactivated)一样组合使用。

启动一次性定时器的方法签名如下,其中 T 是 Actor 协议类型,FiniteDuration 是延迟时长的类型:

less 复制代码
def startSingleTimer(msg: T, delay: FiniteDuration): Unit

通过 import scala.concurrent.duration._,整数可自动转为 FiniteDuration(例如秒),写法如 .second

清单 2.9 WalletTimer 的 activated 行为

scss 复制代码
import scala.concurrent.duration._

object WalletTimer {
   ...

  def activated(total: Int): Behavior[Command] =
    Behaviors.receive { (context, message) =>
      Behaviors.withTimers { timers =>                     ❶
        message match {
          case Increase(amount) =>
            val current = total + amount
            context.log.info(s"increasing to $current")
            resume(current)
          case Deactivate(t) =>
            timers.startSingleTimer(Activate, t.second)    ❷
            deactivated(total)
          case Activate =>
            Behaviors.same
       }
     }
   }
}

❶ 用工厂创建定时器

❷ 调度在 t 秒后发送 Activate。将 t: Int 转为以秒计的 FiniteDuration

钱包的其余部分可以保持不变;加入消息调度后,就具备了新功能。很简单,对吧?

小结(Summary)

  • ActorSystem 是第一个 Actor,同时代表整个系统

  • 你应当优雅终止 ActorSystem,以便系统能关闭所有持有资源的模块(如 HTTP 或数据库连接)。

  • tell 操作符(其简写为bang!)用于向 Actor 发送消息

  • 对于 Actor 处理的每一条消息 ,都需要指定下一条消息使用的行为。由于这个抽象,你只需关心一次处理一条消息,从而消除了并发问题的一个维度。

  • Behaviors 是嵌套的:就像函数链式调用一样,一个行为的输出成为下一个行为的输入,共同构成 Actor 的完整行为。

  • 多条消息通常组成 Actor 的协议(protocol) 。这是 Actor 的 API,也是其他 Actor 与之通信的唯一方式

  • Actor 有两种方式保存状态

    • 作为保存在构造参数中,并在收到消息时更新;
    • 作为行为 保存在多个行为之间(对同类消息作出不同响应),也就是有限状态机(FSM)
  • Behaviors.withTimers 允许你调度消息 ,即指定消息何时以何频率发送。

相关推荐
程序猿不脱发22 小时前
聊聊负载均衡架构
运维·架构·负载均衡
猿java3 小时前
在 Spring中,用id和name命名Bean,究竟有什么区别?
后端·spring·架构
猿java3 小时前
OAuth2是什么?它有哪些授权模式?
后端·安全·架构
就是帅我不改3 小时前
惊!淘宝秒杀系统竟用这种Java并发方案?高并发秒杀架构深度解密
后端·面试·架构
AndrewHZ3 小时前
【芯芯相印】芯片设计生产全流程核心技术术语与实践指南:从架构定义到量产交付的完整图谱
架构·芯片设计·核心技术·技术术语·芯片架构·芯片行业
程序猿阿伟4 小时前
《微服务架构下API网关流量控制Bug复盘:从熔断失效到全链路防护》
微服务·架构·bug
未来之窗软件服务14 小时前
浏览器开发CEFSharp+X86+win7(十三)之Vue架构自动化——仙盟创梦IDE
架构·自动化·vue·浏览器开发·仙盟创梦ide·东方仙盟
chenglin01615 小时前
Logstash——性能、可靠性与扩展性架构
架构
什么都想学的阿超16 小时前
【大语言模型 17】高效Transformer架构革命:Reformer、Linformer、Performer性能突破解析
语言模型·架构·transformer