之前的几部分Fault Tolerance,Dispatch,Mailbox是针对一些特定的应用场景,会需要用到不同的配置,这里先省略一下。至于Routing这一部分,应该是最接近分布式的一部分,放到之后在细细研究。FSM这一部分,需要一定的系统架构方面的知识,我就是看了个热闹。
(一)持久化需要什么?
说到持久化,一定会想到数据库,日志文件(journal),快照(snapshot)之类的。而Actor的持久化同样需要这些东西,在本文中,我们使用的是journal和snapshot来进行Actor的持久化操作。
我们使用Akka提供的持久化工具,需要添加依赖:
"com.typesafe.akka" %% "akka-persistence" % "2.5.6"
我们使用LevelDB来进行journal的保存,添加依赖:
"org.iq80.leveldb" % "leveldb" % "0.9"
"org.fusesource.leveldbjni" % "leveldbjni-all" % "1.8"
添加依赖之后,需要在application.conf文件中队其进行配置:
akka.persistence.journal.plugin = "akka.persistence.journal.leveldb"
akka.persistence.snapshot-store.plugin = "akka.persistence.snapshot-store.local"
akka.persistence.journal.leveldb.dir = "log/journal"
akka.persistence.snapshot-store.local.dir = "log/snapshots"
# DO NOT USE THIS IN PRODUCTION !!!
# See also https://github.com/typesafehub/activator/issues/287
akka.persistence.journal.leveldb.native = false //因为我们本地并没有安装leveldb,所以这个属性置为false,但是生产环境并不推荐使用
(二)持久化的原理
在Akka中,支持持久化的Actor是PersistentActor。需要实现三个方法来完成持久化的操作。
override def persistenceId
这个方法是用来给当前的Actor设定指定的id,通过这个id,Actor将事件保存到指定的journal中,或者保存当前的snapshot。
override def receiveCommand
用于处理Actor收到的所有消息,类似于之前的receive方法。
override def receiveRecover
在Actor重新启动或再次启动的时候,从journal或者snapshot中回复状态。
此外我们还需要一个重要的方法,来将事件进行保存。
persist(Event)(Event handler)
这个函数有两个参数(柯里化),第一个参数表示一个Event(序列化之后保存在journal中),第二个参数是对之前事件参数的处理方法(一些操作)。这里需要强调一下,persist方法先将Event持久化之后,才会执行handler方法,因为handler方法可能会修改Event。这些操作是会阻塞当前Actor继续接受消息,所有到来的消息会被stash。知道persist完成。所以在这里stash可能会溢出,可以使用一些策略防止溢出发生。
akka.actor.default-mailbox.stash-capacity=10000
设置stash的最大容量。
akka.persistence.internal-stash-overflow-strategy=
"akka.persistence.ThrowExceptionConfigurator"
设置stash的溢出策略。
我们来看一下示例代码
package persistent
import akka.actor.{ActorSystem, Props}
import akka.persistence._
// 定义命令
case class Cmd(data: String)
// 定义Event,保存到journal中(文件中)
case class Evt(data: String)
// 状态,保存在Actor内(内存中)
case class ExampleState(events: List[String] = Nil) {
def update(evt: Evt): ExampleState = copy(evt.data :: events)
def size: Int = events.length
override def toString: String = events.reverse.toString()
}
class ExamplePersistentActor extends PersistentActor{
// 状态(一个字符串链表)
var state = ExampleState()
def numEvents = state.size
// Event handler(persist事件的回调函数)
def updateState(event: Evt): Unit ={
state = state.update(event)
}
// 不从snapshot中恢复,只读取journal中的前3条Event
// override def recovery: Recovery = Recovery(fromSnapshot = SnapshotSelectionCriteria.None, toSequenceNr = 3L)
// 不进行恢复(journal和snapshot)
// override def recovery: Recovery = Recovery.none
override def receiveRecover = {
// 在完成恢复和开始接受消息之间,接受到RecoveryCompleted消息
case RecoveryCompleted =>
println("recovery complete")
// 恢复Event
case evt: Evt => updateState(evt)
// 恢复Snapshot
case SnapshotOffer(_, snapshot: ExampleState) => {
println("recover from snapshot")
state = snapshot
}
}
// 设置取snapshot的间隔
val snapShotInterval = 1000
override def receiveCommand = {
// 接收到一条命令(这里就是一个字符串)
case Cmd(data) =>
// 持久化Evnet,使用updateState作为Event handler
persist(Evt(s"${data}-${numEvents}"))(updateState)
// 使用自定义的Event handler
persist(Evt(s"${data}-${numEvents + 1}")){
event =>
updateState(event)
context.system.eventStream.publish(event)
// 取快照
// lastSequenceNr: Highest received sequence number so far or 0L if this actor hasn't replayed or stored any persistent events yet.
// if(lastSequenceNr % snapShotInterval == 0 && lastSequenceNr != 0)
// saveSnapshot(state)
}
// 不进行持久化,但是会调用 Evnet handler
deferAsync(Evt(s"${data}-${numEvents + 2}")){e => println(e)}
case "print" => println(state)
case "snap" => saveSnapshot(state)
}
override def persistenceId = "sample-id-2"
}
object PersistentActorExample extends App{
val system = ActorSystem("example")
val persistentActor = system.actorOf(Props[ExamplePersistentActor], "persistentActor-4-scala")
persistentActor ! "print"
persistentActor ! Cmd("foo")
persistentActor ! Cmd("baz")
persistentActor ! Cmd("bar")
persistentActor ! "snap"
persistentActor ! Cmd("buzz")
persistentActor ! "print"
Thread.sleep(10000)
system.terminate()
}
另外一个持久化方法是
persistAsync(Event)(Event handler)
这个方法会从持久化操作中立刻返回,新到的消息不会被stash
一个更加直接的实例:高throughput下的可持久化
需要注意一点:PersistentActor不能使用PoisonPill进行关闭。
(三)一种Confirm Delivery
在之前对Actor的学习过程中,我们没有考虑Actor出现故障,网络通信受阻等情况,在实际场景下,有时候我们需要可信传输来保证数据的一致性。我们已经学习过了持久化的一些方法,我们就可以使用持久化+AtLeastOneDelivery来实现可信传输。
我们首先需要一个新的特性:
AtLeastOnceDelivery
这个特性为我们提供一个单调增加的deliveryId,我们就是用这个deliveryId来实现可信传输。
Actor通过deliver方法,将消息发送给一个ActorSelection,注意这里不是ActorRef。这个操作会在unconfirm列表中添加新的一项,当超过一定时间没有收到Comfirm消息的情况下,消息会重新发送。
我们看一下实例代码
package confirm
import akka.actor.{Actor, ActorLogging, ActorSelection, ActorSystem, Props}
import akka.persistence.{AtLeastOnceDelivery, PersistentActor}
case class Msg(deliveryId: Long, s: String)
case class Confirm(deliveryId: Long)
sealed trait Evt
case class MsgSent(s: String) extends Evt
case class MsgConfirm(deliveryId: Long) extends Evt
object MyPersistentActor2{
def props(destination: ActorSelection): Props = {
Props(new MyPersistentActor2(destination))
}
}
class MyPersistentActor2(destination: ActorSelection)
extends PersistentActor with AtLeastOnceDelivery with ActorLogging{
def updateState(evt: Evt): Unit = evt match{
case MsgSent(s) =>
log.info(s"sent msg $s to ${destination.pathString}")
deliver(destination)(deliveryId => Msg(deliveryId, s))
case MsgConfirm(deliveryId) =>
log.info(s"receive confirm msg with id $deliveryId")
confirmDelivery(deliveryId)
}
override def receiveCommand = {
case s: String => persist(MsgSent(s))(updateState)
case Confirm(deliveryId) =>
updateState(MsgConfirm(deliveryId))
// persist(MsgConfirm(deliveryId))(updateState)
}
override def receiveRecover = {
case evt: Evt => updateState(evt)
}
override def persistenceId = "persistent-id"
}
class MyDestination extends Actor with ActorLogging {
override def receive = {
case Msg(deliveryId, s) =>
log.info(s"receive message with $deliveryId and $s")
sender() ! Confirm(deliveryId)
}
}
object TestConfirmedDelivery extends App{
val system = ActorSystem("example")
val dest = system.actorOf(Props[MyDestination], "dest")
val persistentActor = system.actorOf(MyPersistentActor2.props(system.actorSelection("/user/dest")))
persistentActor ! "hello"
Thread.sleep(10000)
system.terminate()
}
在可信通信的情况下,我们不光要将发送的消息保存到journal,返回的Confirm消息也需要保存到journal。在恢复的过程中,就不需要再发送之前已经发送并确认过的消息了。(它们会逐一从journal中取出,并重新处理后,更新对应的deliverId在unconfirm表中的状态)。