深入理解 Scala 中的 Akka 演员模型
1. 演员模型基础
演员(Actor)在处理消息时可能会创建新的演员,并且可以改变其处理消息的方式,这实际上实现了状态机中的状态转换。与使用方法调用的传统对象系统不同,演员消息发送通常是异步的,因此操作的全局顺序是不确定的。演员可以控制一些状态,并根据消息对其进行演化。设计良好的演员系统会防止其他代码直接访问和修改这些状态,或者至少会强烈阻止这种做法。
这些特性使得演员可以并行运行,甚至可以跨集群运行。它们提供了一种管理全局状态的原则性方法,在很大程度上(但不是完全)避免了传统多线程并发的问题。
2. Akka:Scala 的演员库
在早期,Scala 自带了一个演员库,但现在这个库已被弃用,Akka 成为了 Scala 中基于演员的并发的官方库。Akka 由 Typesafe 开发和支持,并且还提供了全面的 Java API。
2.1 演员模型的重要实现
演员模型有两个重要的生产就绪实现:Erlang 实现和 Akka。Akka 的灵感来自于 Erlang 的实现,它们都实现了一个重要的创新,即强大的错误处理和恢复模型。
2.2 监督策略
在 Akka 中,不仅会创建演员来执行系统的常规工作,还会创建监督者(Supervisor)来监视一个或多个演员的生命周期。如果一个演员失败(例如抛出异常),监督者会遵循恢复策略,包括重启、关闭、忽略错误或委托给上级处理。
重启策略有两种:
-
全为一策略(All - for - one)
:当失败的演员与其他演员紧密协作,且都在同一个监督者之下时,最好重启所有演员。
-
一对一策略(One - for - one)
:当被管理的演员是独立的工作者,一个演员的失败不会影响其他演员时,只需要重启失败的演员。
这种架构将错误处理逻辑与正常处理逻辑清晰地分离,实现了全架构的错误处理策略,并且提倡“让它崩溃”的原则。
3. 示例介绍
我们将使用一个模拟客户端接口调用服务的示例,该服务将工作委托给工作者。客户端接口(包含 main 方法)名为 AkkaClient,它将用户命令传递给单个 ServerActor,ServerActor 再将工作委托给多个 WorkerActors,以确保不会阻塞。每个工作者模拟一个分片数据存储,维护一个键(Long 类型)和值(String 类型)的映射,并支持 CRUD(创建、读取、更新和删除)语义。
3.1 消息定义
首先,我们来看定义演员之间交换的所有消息的
Messages
对象:
// src/main/scala/progscala2/concurrency/akka/Messages.scala
package progscala2.concurrency.akka
import scala.util.Try
object Messages {
sealed trait Request {
val key: Long
}
case class Create(key: Long, value: String) extends Request
case class Read(key: Long) extends Request
case class Update(key: Long, value: String) extends Request
case class Delete(key: Long) extends Request
case class Response(result: Try[String])
case class Start(numberOfWorkers: Int = 1)
case class Crash(whichOne: Int)
case class Dump(whichOne: Int)
case object DumpAll
}
上述代码中消息的含义如下:
| 消息类型 | 含义 |
| ---- | ---- |
|
Request
| 所有 CRUD 请求的父特征,都使用 Long 类型的键 |
|
Create
| 使用指定的键和值创建一个新的“记录” |
|
Read
| 读取给定键的记录 |
|
Update
| 使用给定键的新值更新记录(如果不存在则创建) |
|
Delete
| 删除给定键的记录(如果不存在则不做任何操作) |
|
Response
| 封装响应,使用
scala.util.Try
表示成功或失败 |
|
Start
| 开始处理,发送给 ServerActor 并告知要创建的工作者数量 |
|
Crash
| 发送消息模拟一个工作者“崩溃” |
|
Dump
| 发送消息“转储”单个工作者或所有工作者的状态 |
|
DumpAll
| 转储所有工作者的状态 |
3.2 AkkaClient 实现
// src/main/scala/progscala2/concurrency/akka/AkkaClient.scala
package progscala2.concurrency.akka
import akka.actor.{ActorRef, ActorSystem, Props}
import java.lang.{NumberFormatException => NFE}
object AkkaClient {
import Messages._
private var system: Option[ActorSystem] = None
def main(args: Array[String]) = {
processArgs(args)
val sys = ActorSystem("AkkaClient")
system = Some(sys)
val server = ServerActor.make(sys)
val numberOfWorkers = sys.settings.config.getInt("server.number-workers")
server ! Start(numberOfWorkers)
processInput(server)
}
private def processArgs(args: Seq[String]): Unit = args match {
case Nil =>
case ("-h" | "--help") +: tail => exit(help, 0)
case head +: tail => exit(s"Unknown input $head!\n"+help, 1)
}
private def processInput(server: ActorRef): Unit = {
val blankRE = """^\s*#?\s*$""".r
val badCrashRE = """^\s*[Cc][Rr][Aa][Ss][Hh]\s*$""".r
val crashRE = """^\s*[Cc][Rr][Aa][Ss][Hh]\s+(\d+)\s*$""".r
val dumpRE = """^\s*[Dd][Uu][Mm][Pp](\s+\d+)?\s*$""".r
val charNumberRE = """^\s*(\w)\s+(\d+)\s*$""".r
val charNumberStringRE = """^\s*(\w)\s+(\d+)\s+(.*)$""".r
def prompt() = print(">> ")
def missingActorNumber() = println("Crash command requires an actor number.")
def invalidInput(s: String) = println(s"Unrecognized command: $s")
def invalidCommand(c: String): Unit = println(s"Expected 'c', 'r', 'u', or 'd'. Got $c")
def invalidNumber(s: String): Unit = println(s"Expected a number. Got $s")
def expectedString(): Unit = println("Expected a string after the command and number")
def unexpectedString(c: String, n: Int): Unit = println(s"Extra arguments after command and number '$c $n'")
def finished(): Nothing = exit("Goodbye!", 0)
val handleLine: PartialFunction[String,Unit] = {
case blankRE() => // do nothing
case "h" | "help" => println(help)
case dumpRE(n) => server ! (if (n == null) DumpAll else Dump(n.trim.toInt))
case badCrashRE() => missingActorNumber()
case crashRE(n) => server ! Crash(n.toInt)
case charNumberStringRE(c, n, s) => c match {
case "c" | "C" => server ! Create(n.toInt, s)
case "u" | "U" => server ! Update(n.toInt, s)
case "r" | "R" => unexpectedString(c, n.toInt)
case "d" | "D" => unexpectedString(c, n.toInt)
case _ => invalidCommand(c)
}
case charNumberRE(c, n) => c match {
case "r" | "R" => server ! Read(n.toInt)
case "d" | "D" => server ! Delete(n.toInt)
case "c" | "C" => expectedString
case "u" | "U" => expectedString
case _ => invalidCommand(c)
}
case "q" | "quit" | "exit" => finished()
case string => invalidInput(string)
}
while (true) {
prompt()
Console.in.readLine() match {
case null => finished()
case line => handleLine(line)
}
}
}
private val help = """Usage: AkkaClient [-h | --help]
|Then, enter one of the following commands, one per line:
| h | help Print this help message.
| c n string Create "record" for key n for value string.
| r n Read record for key n. It's an error if n isn't found.
| u n string Update (or create) record for key n for value string.
| d n Delete record for key n. It's an error if n isn't found.
| crash n "Crash" worker n (to test recovery).
| dump [n] Dump the state of all workers (default) or worker n.
| ^d | quit Quit.
|""".stripMargin
private def exit(message: String, status: Int): Nothing = {
for (sys <- system) sys.shutdown()
println(message)
sys.exit(status)
}
}
2.3 配置文件
Akka 使用 Typesafe 的 Config 库进行配置,配置文件如下:
// src/main/resources/application.conf
akka {
loggers = [akka.event.slf4j.Slf4jLogger]
loglevel = debug
actor {
debug {
unhandled = on
lifecycle = on
}
}
}
server {
number-workers = 5
}
2.4 处理流程
以下是 AkkaClient 的处理流程:
graph TD;
A[开始] --> B[处理命令行参数];
B --> C[创建 ActorSystem];
C --> D[创建 ServerActor];
D --> E[确定工作者数量];
E --> F[发送 Start 消息];
F --> G[处理用户输入];
G --> H{是否退出};
H -- 否 --> G;
H -- 是 --> I[关闭 ActorSystem];
I --> J[退出程序];
2.5 代码解释
-
processArgs方法用于处理命令行参数,支持-h或--help选项。 -
processInput方法使用正则表达式解析用户输入,并根据输入发送相应的消息给ServerActor。 -
help变量包含详细的帮助信息。 -
exit方法用于关闭ActorSystem并退出程序。
4. ServerActor 实现
// src/main/scala/progscala2/concurrency/akka/ServerActor.scala
package progscala2.concurrency.akka
import scala.util.{Try, Success, Failure}
import scala.util.control.NonFatal
import scala.concurrent.duration._
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
import akka.actor.{Actor, ActorLogging, ActorRef,
ActorSystem, Props, OneForOneStrategy, SupervisorStrategy}
import akka.pattern.ask
import akka.util.Timeout
class ServerActor extends Actor with ActorLogging {
import Messages._
implicit val timeout = Timeout(1.seconds)
override val supervisorStrategy: SupervisorStrategy = {
val decider: SupervisorStrategy.Decider = {
case WorkerActor.CrashException => SupervisorStrategy.Restart
case NonFatal(ex) => SupervisorStrategy.Resume
}
OneForOneStrategy()(decider orElse super.supervisorStrategy.decider)
}
var workers = Vector.empty[ActorRef]
def receive = initial
val initial: Receive = {
case Start(numberOfWorkers) =>
workers = ((1 to numberOfWorkers) map makeWorker).toVector
context become processRequests
}
val processRequests: Receive = {
case c @ Crash(n) => workers(n % workers.size) ! c
case DumpAll =>
Future.fold(workers map (_ ? DumpAll))(Vector.empty[Any])(_ :+ _)
.onComplete(askHandler("State of the workers"))
case Dump(n) =>
(workers(n % workers.size) ? DumpAll).map(Vector(_))
.onComplete(askHandler(s"State of worker $n"))
case request: Request =>
val key = request.key.toInt
val index = key % workers.size
workers(index) ! request
case Response(Success(message)) => printResult(message)
case Response(Failure(ex)) => printResult(s"ERROR! $ex")
}
def askHandler(prefix: String): PartialFunction[Try[Any],Unit] = {
case Success(suc) => suc match {
case vect: Vector[_] =>
printResult(s"$prefix:\n")
vect foreach {
case Response(Success(message)) =>
printResult(s"$message")
case Response(Failure(ex)) =>
printResult(s"ERROR! Success received wrapping $ex")
}
case _ => printResult(s"BUG! Expected a vector, got $suc")
}
case Failure(ex) => printResult(s"ERROR! $ex")
}
protected def printResult(message: String) = {
println(s"<< $message")
}
protected def makeWorker(i: Int) =
context.actorOf(Props[WorkerActor], s"worker-$i")
}
object ServerActor {
def make(system: ActorSystem): ActorRef =
system.actorOf(Props[ServerActor], "server")
}
4.1 代码解释
-
监督策略
:
supervisorStrategy方法重写了默认的监督策略,使用一对一策略。如果发生模拟崩溃(WorkerActor.CrashException),则重启演员;如果发生其他非致命异常,则继续执行。 -
消息处理
:
-
initial处理Start消息,创建工作者并切换到processRequests状态。 -
processRequests处理其他消息,包括Crash、DumpAll、Dump、Request和Response。
-
-
辅助方法
:
-
askHandler处理Future的完成结果。 -
printResult打印处理结果。 -
makeWorker创建工作者演员。
-
4.2 ServerActor 处理流程
graph TD;
A[接收到消息] --> B{是否为 Start 消息};
B -- 是 --> C[创建工作者];
C --> D[切换到 processRequests 状态];
B -- 否 --> E{是否为 Crash 消息};
E -- 是 --> F[发送 Crash 消息给对应工作者];
E -- 否 --> G{是否为 DumpAll 消息};
G -- 是 --> H[收集所有工作者状态];
G -- 否 --> I{是否为 Dump 消息};
I -- 是 --> J[收集指定工作者状态];
I -- 否 --> K{是否为 Request 消息};
K -- 是 --> L[转发请求给对应工作者];
K -- 否 --> M{是否为 Response 消息};
M -- 是 --> N[打印结果];
M -- 否 --> O[未知消息处理];
5. WorkerActor 实现
// src/main/scala/progscala2/concurrency/akka/WorkerActor.scala
package progscala2.concurrency.akka
import scala.util.{Try, Success, Failure}
import akka.actor.{Actor, ActorLogging}
class WorkerActor extends Actor with ActorLogging {
import Messages._
private val datastore = collection.mutable.Map.empty[Long,String]
def receive = {
case Create(key, value) =>
datastore += key -> value
sender ! Response(Success(s"$key -> $value added"))
case Read(key) =>
sender ! Response(Try(s"${datastore(key)} found for key = $key"))
case Update(key, value) =>
datastore += key -> value
sender ! Response(Success(s"$key -> $value updated"))
case Delete(key) =>
datastore -= key
sender ! Response(Success(s"$key deleted"))
case Crash(_) => throw WorkerActor.CrashException
case DumpAll =>
sender ! Response(Success(s"${self.path}: datastore = $datastore"))
}
}
object WorkerActor {
case object CrashException extends RuntimeException("Crash!")
}
5.1 代码解释
-
datastore是一个可变的键值对映射,用于存储数据。 -
receive方法处理不同的消息:-
Create:添加新的键值对。 -
Read:尝试读取键对应的值。 -
Update:更新键对应的值。 -
Delete:删除键值对。 -
Crash:抛出CrashException模拟崩溃。 -
DumpAll:返回数据存储的状态。
-
5.2 WorkerActor 处理流程
graph TD;
A[接收到消息] --> B{是否为 Create 消息};
B -- 是 --> C[添加键值对];
C --> D[发送成功响应];
B -- 否 --> E{是否为 Read 消息};
E -- 是 --> F[尝试读取值];
F --> G[发送响应];
E -- 否 --> H{是否为 Update 消息};
H -- 是 --> I[更新键值对];
I --> J[发送成功响应];
H -- 否 --> K{是否为 Delete 消息};
K -- 是 --> L[删除键值对];
L --> M[发送成功响应];
K -- 否 --> N{是否为 Crash 消息};
N -- 是 --> O[抛出异常];
N -- 否 --> P{是否为 DumpAll 消息};
P -- 是 --> Q[发送数据存储状态];
P -- 否 --> R[未知消息处理];
6. 运行示例
6.1 运行命令
在 sbt 提示符下运行:
run-main progscala2.concurrency.akka.AkkaClient
或者使用
run
并从列表中选择。输入
h
查看命令列表并尝试几个命令,使用
quit
退出。
也可以使用以下命令从 shell 或命令窗口运行命令文件:
sbt "run-main progscala2.concurrency.akka.AkkaClient" < misc/run-akka-input.txt
6.2 注意事项
- 由于操作本质上是异步的,每次运行脚本或复制粘贴输入行时会看到不同的结果。
- 当演员崩溃时,数据会丢失。如果这不可接受,可以使用 Akka Persistence 模块进行持久化。
-
ServerActor通过ActorRef访问演员,确保即使演员崩溃,引用仍然有效。
通过上述示例,我们可以看到 Akka 演员模型在处理并发和错误处理方面的强大功能,能够有效地管理系统状态和处理异常情况。
深入理解Scala中Akka演员模型
超级会员免费看
75

被折叠的 条评论
为什么被折叠?



