很高兴继续在Manuel Bernhardt的jOOQ博客上做客串文章。 在本博客系列中,Manuel将解释所谓的反应技术背后的动机,并在介绍期货和参与者的概念后使用Actor来与jOOQ一起访问关系数据库。
Manuel Bernhardt是一位独立软件顾问,热衷于构建基于Web的后端和前端系统。 他是“响应式Web应用程序” (Manning)的作者,在Java花费了很长时间之后,他于2010年开始与Scala,Akka和Play Framework合作。 他住在维也纳,他是当地Scala用户组的联合组织者。 他对基于Scala的技术和充满活力的社区充满热情,并正在寻找在行业中推广其使用的方法。 自6岁起,他就开始潜水,不能完全适应奥地利缺少海的情况。
本系列分为三个部分,我们将在下个月发布:
- 第1部分:为什么要反应,为什么要“异步”和期货简介
- 第2部分:演员简介
- 第3部分:将jOOQ与Scala,期货和参与者一起使用
介绍
在我们的上一篇文章中,我们介绍了反应式应用程序的概念 ,解释了异步编程的优点,并介绍了Futures(一种用于表达和操作异步值的工具)。
在本文中,我们将研究基于消息驱动的通信概念构建异步程序的另一个工具:参与者。
基于Actor的并发模型已被Erlang编程语言推广,其在JVM上最流行的实现是Akka并发工具包 。
在某种程度上,Actor模型是“正确”的面向对象的:actor的状态可以是可变的,但永远不会直接暴露给外界。 相反,参与者在异步消息传递的基础上彼此通信,在异步消息传递中消息本身是不可变的。 演员只能做以下三件事之一:
- 发送和接收任意数量的消息
- 更改其行为或状态以响应消息到达
- 开始新的儿童演员
演员通常要决定准备共享什么状态以及何时对其进行突变。 因此,此模型使我们的人更容易编写并发程序,而这些程序不会因意外读取或写入过时状态或使用锁来避免这种情况而引入竞争条件或死锁。
接下来,我们将看到Actor的工作方式以及如何将它们与Future相结合。
演员基础
Actor是轻量级的对象,它们通过发送和接收消息相互通信。 每个参与者都有一个邮箱 ,传入的消息在得到处理之前会在其中排队。
角色具有不同的状态:可以启动,恢复,停止和重新启动。 当actor崩溃时,恢复或重启actor很有用,我们将在后面看到。
演员也有演员参考 ,这是一个演员接触另一个演员的一种手段。 像电话号码一样,actor引用是指向actor的指针,如果在崩溃的情况下重新启动actor并用新的化身替换actor,则与其他尝试向其发送消息的actor一样没有任何区别。他们所知道的关于演员的事情是它的参考,而不是一个特定化身的身份。
发送和接收消息
让我们从创建一个简单的actor开始:
import akka.actor._
class Luke extends Actor {
def receive = {
case _ => // do nothing
}
}
这实际上就是创建一个演员所需要的全部。 但这不是很有趣。 让我们加一点香料,并定义对给定消息的反应:
import akka.actor._
case object RevelationOfFathership
class Luke extends Actor {
def receive = {
case RevelationOfFathership =>
System.err.println("Noooooooooo")
}
}
开始了! RevelationOfFathership
是一个案例对象,即不可变的消息。 最后一个细节非常重要:您的消息应该始终是自包含的,并且不要引用任何参与者的内部状态,因为这会将这种状态有效地泄漏到外部,从而破坏了只有参与者可以更改其内部状态的保证。 最后一点对于参与者提供更好,更人性化的并发模型并且不会引起任何意外是至关重要的。
现在,卢克(Luke)知道如何恰当地回应黑暗维德(Dark Vader)是他父亲的不便事实,我们所需要的只是黑暗君主本人。
import akka.actor._
class Vader extends Actor {
override def preStart(): Unit =
context.actorSelection("akka://application/user/luke") ! RevelationOfFathership
def receive = {
case _ => // ...
}
}
维达演员使用preStart
生命周期方法,以便在儿子上手时触发将消息发送给他的儿子。 我们使用演员的上下文来向Luke发送消息。
运行此示例的整个序列如下所示:
import akka.actor._
val system = ActorSystem("application")
val luke = system.actorOf(Props[Luke], name = "luke")
val vader = system.actorOf(Props[Vader], name = "vader")
Props
是描述如何获取演员实例的方法。 由于它们是不可变的,因此可以自由共享它们,例如,跨运行在不同计算机上的不同JVM(例如,在操作Akka群集时,这很有用)。
演员监督
演员不仅在野外存在,而且是演员层次结构的一部分,每个演员都有一个父级。 我们创建的Actor由应用程序ActorSystem的User Guardian进行监督,ActorSystem是Akka提供的特殊actor,负责监督用户空间中的所有actor。 监督演员的角色是决定如何处理儿童演员的失败并采取相应的行动。
用户守护者本身由Root Guardian(也监督Akka内部的另一个特殊角色)进行监督,并由特殊角色引用对其进行监督。 传奇人物说,这个参考文献是在所有其他演员参考文献出现之前就存在的,被称为“走时空泡沫的那个人”(如果您不相信我,请查看Akka官方文档 )。
在层次结构中组织参与者提供了将错误处理编码到层次结构中的优势。 每个父母都应对自己子女的行为负责。 万一出问题了并且孩子崩溃了,父母将有机会重新启动它。
例如,维达(Vader)有一些突击部队:
import akka.actor._
import akka.routing._
class Vader extends Actor {
val troopers: ActorRef = context.actorOf(
RoundRobinPool(8).props(Props[StromTrooper])
)
}
RoundRobinPool
是一种表达以下事实的方式:发送到troopers
消息将一个接一个地发送给每个部队的孩子。 路由器对将消息一次发送给多个参与者的策略进行编码,Akka提供了许多预定义的路由器。
最终,演员可能会崩溃,然后由主管决定要做什么。 决策机制以所谓的监督策略为代表。 例如,维达(Vader)可以决定在放弃并停止风暴突击队3次之前,重新尝试重新启动它:
import akka.actor._
class Vader extends Actor {
val troopers: ActorRef = context.actorOf(
RoundRobinPool(8).props(Props[StromTrooper])
)
override def supervisorStrategy =
OneForOneStrategy(maxNrOfRetries = 3) {
case t: Throwable =>
log.error("StormTrooper down!", t)
SupervisorStrategy.Restart
}
}
这种监管策略相当粗糙,因为它以相同的方式处理所有类型的Throwable
。 在下一篇文章中,我们将看到监督策略是一种以不同方式应对不同类型故障的有效手段。
结合期货和参与者
与actor合作有一个黄金法则:您不应执行任何阻止操作,例如阻止网络呼叫。 原因很简单:如果actor阻塞,它将无法处理可能导致邮箱已满的传入消息(或者,由于actor使用的默认邮箱不受限制,因此会OutOfMemoryException
。
这就是为什么在参与者中使用Future可能有用的原因。 管道模式旨在实现这一目的:它将Future的结果发送给actor:
import akka.actor._
import akka.pattern.pipe
class Luke extends Actor {
def receive = {
case RevelationOfFathership =>
sendTweet("Nooooooo") pipeTo self
case tsr: TweetSendingResult =>
// ...
}
def sendTweet(msg: String): Future[TweetSendingResult] = ...
}
在此示例中,我们在接收到RevelationOfFathership
后调用sendTweet
Future,并使用pipeTo
方法指示我们希望将Future的结果发送给自己。
上面的代码只有一个问题:如果Future失败了,我们将以一种非常不方便的格式接收失败的throwable,包装在akka.actor.Status.Failure
类型的消息中,没有任何有用的上下文。 这就是为什么在通过管道传递结果之前恢复故障可能更合适的原因:
import akka.actor._
import akka.pattern.pipe
import scala.control.NonFatal
class Luke extends Actor {
def receive = {
case RevelationOfFathership =>
val message = "Nooooooo"
sendTweet(message) recover {
case NonFatal(t) => TweetSendFailure(message, t)
} pipeTo self
case tsr: TweetSendingResult =>
// ...
case tsf: TweetSendingFailure =>
// ...
}
def sendTweet(msg: String): Future[TweetSendingResult] = ...
}
通过这种故障处理,我们现在知道哪些消息未能在Twitter上发送,并且可以采取适当的措施(例如,重试发送)。
简短介绍Actors就是这样。 在本系列的下一篇和最后一篇文章中,我们将了解如何结合使用Futures和Actors进行反应式数据库访问。
继续阅读
请继续关注,我们将很快发布第3部分,作为本系列的一部分:
- 第1部分:为什么要反应,为什么要“异步”和期货简介
- 第2部分:演员简介
- 第3部分:将jOOQ与Scala,期货和参与者一起使用
翻译自: https://www.javacodegeeks.com/2015/12/reactive-database-access-part-2-actors.html