前面提到过,akka-typed中较重要的改变是加入了EventSourcedBehavior。也就是说增加了一种专门负责EventSource模式的actor, 最终和其它种类的actor一道可以完美实现CQRS。新的actor,我还是把它称为persistentActor,还是一种能维护和维持运行状态的actor。即,actor内部状态可以存放在数据库里,然后通过一组功能函数来提供对状态的处理转变,即持续化处理persistence。当然作为一种具备EventSourcedBehavior的actor, 普遍应有的actor属性、方法、消息处理协议、监管什么的都还必须存在。在这篇讨论里我们就通过案例和源码来说明一下EventSourcedBehavior是如何维护内部状态及作为一种actor又应该怎么去使用它。

我们把上一篇讨论里购物车的例子拿来用,再增加一些消息回复response机制,主要是汇报购物车状态:

object ItemInfo {
case class Item(name: String, price: Double)
} object MyCart {
import ItemInfo._ sealed trait Command
sealed trait Event extends CborSerializable
sealed trait Response //commands
case class AddItem(item: Item) extends Command
case object PayCart extends Command
case class CountItems(replyTo: ActorRef[Response]) extends Command //event
case class ItemAdded(item: Item) extends Event
case object CartPaid extends Event //state
case class CartLoad(load: List[Item] = Nil) //response
case class PickedItems(items: List[Item]) extends Response
case object CartEmpty extends Response val commandHandler: (CartLoad, Command) => Effect[Event,CartLoad] = { (state, cmd) =>
cmd match {
case AddItem(item) =>
Effect.persist(ItemAdded(item))
case PayCart =>
Effect.persist(CartPaid)
case CountItems(replyTo) =>
Effect.none.thenRun { cart =>
cart.load match {
case Nil =>
replyTo ! CartEmpty
case listOfItems =>
replyTo ! PickedItems(listOfItems)
}
}
}
} val eventHandler: (CartLoad,Event) => CartLoad = { (state,evt) =>
evt match {
case ItemAdded(item) =>
state.copy(load = item :: state.load)
case CartPaid =>
state.copy(load = Nil)
}
} def apply(): Behavior[Command] = EventSourcedBehavior[Command,Event,CartLoad](
persistenceId = PersistenceId("",""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
) } object Shopper { import ItemInfo._ sealed trait Command extends CborSerializable case class GetItem(item: Item) extends Command
case object Settle extends Command
case object GetCount extends Command case class WrappedResponse(res: MyCart.Response) extends Command def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
val shoppingCart = ctx.spawn(MyCart(), "shopping-cart")
val cartRef: ActorRef[MyCart.Response] = ctx.messageAdapter(WrappedResponse)
Behaviors.receiveMessage { msg =>
msg match {
case GetItem(item) =>
shoppingCart ! MyCart.AddItem(item)
case Settle =>
shoppingCart ! MyCart.PayCart
case GetCount =>
shoppingCart ! MyCart.CountItems(cartRef)
case WrappedResponse(res) => res match {
case MyCart.PickedItems(items) =>
ctx.log.info("**************Current Items in Cart: {}*************", items)
case MyCart.CartEmpty =>
ctx.log.info("**************shopping cart is empty!***************")
}
}
Behaviors.same
}
} } object ShoppingCart extends App {
import ItemInfo._
val shopper = ActorSystem(Shopper(),"shopper")
shopper ! Shopper.GetItem(Item("banana",11.20))
shopper ! Shopper.GetItem(Item("watermelon",4.70))
shopper ! Shopper.GetCount
shopper ! Shopper.Settle
shopper ! Shopper.GetCount
scala.io.StdIn.readLine() shopper.terminate() }

实际上EventSourcedBehavior里还嵌入了回复机制,完成一项Command处理后必须回复指令方,否则程序无法通过编译。如下:

private def withdraw(acc: OpenedAccount, cmd: Withdraw): ReplyEffect[Event, Account] = {
if (acc.canWithdraw(cmd.amount))
Effect.persist(Withdrawn(cmd.amount)).thenReply(cmd.replyTo)(_ => Confirmed)
else
Effect.reply(cmd.replyTo)(Rejected(s"Insufficient balance ${acc.balance} to be able to withdraw ${cmd.amount}"))
}

不过这个回复机制是一种副作用。即,串连在Effect产生之后立即实施。这个动作是在eventHandler之前。在这个时段无法回复最新的状态。

说到side-effect, 如Effect.persist().thenRun(produceSideEffect): 当成功持续化event后可以安心进行一些其它的操作。例如,当影响库存数的event被persist后可以马上从账上扣减库存。

在上面这个ShoppingCart例子里我们没有发现状态转换代码如Behaviors.same。这只能是EventSourcedBehavior属于更高层次的Behavior,状态转换已经嵌入在eventHandler里了,还记着这个函数的款式吧  (State,Event) => State, 这个State就是状态了。

Events persist在journal里,如果persist操作中journal出现异常,EventSourcedBehavior自备了安全监管策略,如下:

  def apply(): Behavior[Command] = EventSourcedBehavior[Command,Event,CartLoad](
persistenceId = PersistenceId("",""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds))

值得注意的是:这个策略只适用于onPersistFailure(),从外部用Behaviors.supervisor()包嵌是无法实现处理PersistFailure效果的。但整个actor还是需要一种Backoff策略,因为在EventSourcedBehavior内部commandHandler,eventHandler里可能也会涉及一些数据库操作。在操作失败后需要某种Backoff重启策略。那么我们可以为actor增加监控策略如下:

  def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds))
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)

现在这个MyCart可以说已经是个安全、强韧性的actor了。

既然是一种persistentActor,那么持久化的管理应该也算是核心功能了。EventSourcedBehavior通过接收信号提供了对持久化过程监控功能,如:

 def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
).receiveSignal {
case (state, RecoveryCompleted) =>
ctx.log.info("**************Recovery Completed with state: {}***************",state)
case (state, SnapshotCompleted(meta)) =>
ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
case (state,RecoveryFailed(err)) =>
ctx.log.error("recovery failed with: {}",err.getMessage)
case (state,SnapshotFailed(meta,err)) =>
ctx.log.error("snapshoting failed with: {}",err.getMessage)
}
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)

EventSourcedBehavior.receiveSignal是个偏函数:

  def receiveSignal(signalHandler: PartialFunction[(State, Signal), Unit]): EventSourcedBehavior[Command, Event, State]

下面是一个EventSourcedBehavior Signal 清单:

sealed trait EventSourcedSignal extends Signal

@DoNotInherit sealed abstract class RecoveryCompleted extends EventSourcedSignal
case object RecoveryCompleted extends RecoveryCompleted {
def instance: RecoveryCompleted = this
} final case class RecoveryFailed(failure: Throwable) extends EventSourcedSignal {
def getFailure(): Throwable = failure
} final case class SnapshotCompleted(metadata: SnapshotMetadata) extends EventSourcedSignal {
def getSnapshotMetadata(): SnapshotMetadata = metadata
} final case class SnapshotFailed(metadata: SnapshotMetadata, failure: Throwable) extends EventSourcedSignal { def getFailure(): Throwable = failure
def getSnapshotMetadata(): SnapshotMetadata = metadata
} object SnapshotMetadata { /**
* @param persistenceId id of persistent actor from which the snapshot was taken.
* @param sequenceNr sequence number at which the snapshot was taken.
* @param timestamp time at which the snapshot was saved, defaults to 0 when unknown.
* in milliseconds from the epoch of 1970-01-01T00:00:00Z.
*/
def apply(persistenceId: String, sequenceNr: Long, timestamp: Long): SnapshotMetadata =
new SnapshotMetadata(persistenceId, sequenceNr, timestamp)
} /**
* Snapshot metadata.
*
* @param persistenceId id of persistent actor from which the snapshot was taken.
* @param sequenceNr sequence number at which the snapshot was taken.
* @param timestamp time at which the snapshot was saved, defaults to 0 when unknown.
* in milliseconds from the epoch of 1970-01-01T00:00:00Z.
*/
final class SnapshotMetadata(val persistenceId: String, val sequenceNr: Long, val timestamp: Long) {
override def toString: String =
s"SnapshotMetadata($persistenceId,$sequenceNr,$timestamp)"
} final case class DeleteSnapshotsCompleted(target: DeletionTarget) extends EventSourcedSignal {
def getTarget(): DeletionTarget = target
} final case class DeleteSnapshotsFailed(target: DeletionTarget, failure: Throwable) extends EventSourcedSignal {
def getFailure(): Throwable = failure
def getTarget(): DeletionTarget = target
} final case class DeleteEventsCompleted(toSequenceNr: Long) extends EventSourcedSignal {
def getToSequenceNr(): Long = toSequenceNr
} final case class DeleteEventsFailed(toSequenceNr: Long, failure: Throwable) extends EventSourcedSignal {
def getFailure(): Throwable = failure
def getToSequenceNr(): Long = toSequenceNr
}

当然,EventSourcedBehavior之所以能具备自我修复能力其中一项是因为它有对持久化的事件重演机制。如果每次启动都需要对所有历史事件进行重演的话会很不现实。必须用snapshot来浓缩历史事件:

  def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
).receiveSignal {
case (state, RecoveryCompleted) =>
ctx.log.info("**************Recovery Completed with state: {}***************",state)
case (state, SnapshotCompleted(meta)) =>
ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
case (state,RecoveryFailed(err)) =>
ctx.log.error("recovery failed with: {}",err.getMessage)
case (state,SnapshotFailed(meta,err)) =>
ctx.log.error("snapshoting failed with: {}",err.getMessage)
}.snapshotWhen {
case (state,CartPaid,seqnum) =>
ctx.log.info("*****************snapshot taken at: {} with state: {}",seqnum,state)
true
case (state,event,seqnum) => false
}.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = , keepNSnapshots = ))
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)

下面是本次示范的源码:

build.sbt

name := "learn-akka-typed"

version := "0.1"

scalaVersion := "2.13.1"
scalacOptions in Compile ++= Seq("-deprecation", "-feature", "-unchecked", "-Xlog-reflective-calls", "-Xlint")
javacOptions in Compile ++= Seq("-Xlint:unchecked", "-Xlint:deprecation") val AkkaVersion = "2.6.5"
val AkkaPersistenceCassandraVersion = "1.0.0" libraryDependencies ++= Seq(
"com.typesafe.akka" %% "akka-cluster-sharding-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-persistence-typed" % AkkaVersion,
"com.typesafe.akka" %% "akka-persistence-query" % AkkaVersion,
"com.typesafe.akka" %% "akka-serialization-jackson" % AkkaVersion,
"com.typesafe.akka" %% "akka-persistence-cassandra" % AkkaPersistenceCassandraVersion,
"com.typesafe.akka" %% "akka-slf4j" % AkkaVersion,
"ch.qos.logback" % "logback-classic" % "1.2.3"
)

application.conf

akka.actor.allow-java-serialization = on
akka {
loglevel = DEBUG
actor {
serialization-bindings {
"com.learn.akka.CborSerializable" = jackson-cbor
}
}
# use Cassandra to store both snapshots and the events of the persistent actors
persistence {
journal.plugin = "akka.persistence.cassandra.journal"
snapshot-store.plugin = "akka.persistence.cassandra.snapshot"
} }
akka.persistence.cassandra {
# don't use autocreate in production
journal.keyspace = "poc"
journal.keyspace-autocreate = on
journal.tables-autocreate = on
snapshot.keyspace = "poc_snapshot"
snapshot.keyspace-autocreate = on
snapshot.tables-autocreate = on
} datastax-java-driver {
basic.contact-points = ["192.168.11.189:9042"]
basic.load-balancing-policy.local-datacenter = "datacenter1"
}

ShoppingCart.scala

package com.learn.akka

import akka.actor.typed._
import akka.persistence.typed._
import akka.actor.typed.scaladsl.Behaviors
import akka.persistence.typed.scaladsl._
import scala.concurrent.duration._ object ItemInfo {
case class Item(name: String, price: Double)
} object MyCart {
import ItemInfo._ sealed trait Command
sealed trait Event extends CborSerializable
sealed trait Response //commands
case class AddItem(item: Item) extends Command
case object PayCart extends Command
case class CountItems(replyTo: ActorRef[Response]) extends Command //event
case class ItemAdded(item: Item) extends Event
case object CartPaid extends Event //state
case class CartLoad(load: List[Item] = Nil) //response
case class PickedItems(items: List[Item]) extends Response
case object CartEmpty extends Response val commandHandler: (CartLoad, Command) => Effect[Event,CartLoad] = { (state, cmd) =>
cmd match {
case AddItem(item) =>
Effect.persist(ItemAdded(item))
case PayCart =>
Effect.persist(CartPaid)
case CountItems(replyTo) =>
Effect.none.thenRun { cart =>
cart.load match {
case Nil =>
replyTo ! CartEmpty
case listOfItems =>
replyTo ! PickedItems(listOfItems)
}
}
}
} val eventHandler: (CartLoad,Event) => CartLoad = { (state,evt) =>
evt match {
case ItemAdded(item) =>
state.copy(load = item :: state.load)
case CartPaid =>
state.copy(load = Nil)
}
} def apply(): Behavior[Command] =
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
EventSourcedBehavior[Command, Event, CartLoad](
persistenceId = PersistenceId("", ""),
emptyState = CartLoad(),
commandHandler = commandHandler,
eventHandler = eventHandler
).onPersistFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
).receiveSignal {
case (state, RecoveryCompleted) =>
ctx.log.info("**************Recovery Completed with state: {}***************",state)
case (state, SnapshotCompleted(meta)) =>
ctx.log.info("**************Snapshot Completed with state: {},id({},{})***************",state,meta.persistenceId, meta.sequenceNr)
case (state,RecoveryFailed(err)) =>
ctx.log.error("recovery failed with: {}",err.getMessage)
case (state,SnapshotFailed(meta,err)) =>
ctx.log.error("snapshoting failed with: {}",err.getMessage)
}.snapshotWhen {
case (state,CartPaid,seqnum) =>
ctx.log.info("*****************snapshot taken at: {} with state: {}",seqnum,state)
true
case (state,event,seqnum) => false
}.withRetention(RetentionCriteria.snapshotEvery(numberOfEvents = , keepNSnapshots = ))
}
).onFailure(
SupervisorStrategy
.restartWithBackoff(minBackoff = .seconds, maxBackoff = .seconds, randomFactor = 0.1)
.withMaxRestarts()
.withResetBackoffAfter(.seconds)
)
} object Shopper { import ItemInfo._ sealed trait Command extends CborSerializable case class GetItem(item: Item) extends Command
case object Settle extends Command
case object GetCount extends Command case class WrappedResponse(res: MyCart.Response) extends Command def apply(): Behavior[Command] = Behaviors.setup[Command] { ctx =>
val shoppingCart = ctx.spawn(MyCart(), "shopping-cart")
val cartRef: ActorRef[MyCart.Response] = ctx.messageAdapter(WrappedResponse)
Behaviors.receiveMessage { msg =>
msg match {
case GetItem(item) =>
shoppingCart ! MyCart.AddItem(item)
case Settle =>
shoppingCart ! MyCart.PayCart
case GetCount =>
shoppingCart ! MyCart.CountItems(cartRef)
case WrappedResponse(res) => res match {
case MyCart.PickedItems(items) =>
ctx.log.info("**************Current Items in Cart: {}*************", items)
case MyCart.CartEmpty =>
ctx.log.info("**************shopping cart is empty!***************")
}
}
Behaviors.same
}
} } object ShoppingCart extends App {
import ItemInfo._
val shopper = ActorSystem(Shopper(),"shopper")
shopper ! Shopper.GetItem(Item("banana",11.20))
shopper ! Shopper.GetItem(Item("watermelon",4.70))
shopper ! Shopper.GetCount
shopper ! Shopper.Settle
shopper ! Shopper.GetCount
scala.io.StdIn.readLine() shopper.terminate() }

akka-typed(4) - EventSourcedBehavior in action的更多相关文章

  1. Akka Typed 官方文档之随手记

    ️ 引言 近两年,一直在折腾用FP与OO共存的编程语言Scala,采取以函数式编程为主的方式,结合TDD和BDD的手段,采用Domain Driven Design的方法学,去构造DDDD应用(Dom ...

  2. Akka源码分析-Akka Typed

    对不起,akka typed 我是不准备进行源码分析的,首先这个库的API还没有release,所以会may change,也就意味着其概念和设计包括API都会修改,基本就没有再深入分析源码的意义了. ...

  3. Akka Typed系列:协议&行为

    引言 2019年11月6号LightBend公司发布了AKKA 2.6版本,带来了类型安全的actor,新的Akka Cluster底层通信设施——Artery,带来了更好的稳定性,使用Jackson ...

  4. Akka Essentials - 2

    Actors Defining an actor class MyActor extends Actor { def receive = { } } In Scala, the receive blo ...

  5. 怎样在 Akka Persistence 中实现分页查询

    在 Akka Persistence 中,数据都缓存在服务内存(状态),后端存储的都是一些持久化的事件日志,没法使用类似 SQL 一样的 DSL 来进行分页查询.利用 Akka Streams 和 A ...

  6. 一图看懂Actor Typed

    引言 朋友看罢我之前整理的<Akka Typed 官方文档之随手记>,一人用了诗歌<长城长>作为回赠,另一人则要求推出简化版本.于是抽空整理了几张思维导图,并且用了一些不太恰当 ...

  7. Lagom 官方文档之随手记

    引言 Lagom是出品Akka的Lightbend公司推出的一个微服务框架,目前最新版本为1.6.2.Lagom一词出自瑞典语,意为"适量". https://www.lagomf ...

  8. CQRS与Event Sourcing之浅见

    引言 DDD是近年软件设计的热门.CQRS与Event Sourcing作为实施DDD的一种选择,也逐步进入人们的视野.围绕这两个主题,软件开发的大咖[Martin Fowler].[Greg You ...

  9. 网页样式——各种炫酷效果持续更新ing...

    1.evanyou效果-彩带的实现,效果如下 注:这个主要用的是Canvas画布实现的,点击背景绘制新的图形,代码如下: /*Html代码:*/ <canvas id=">< ...

随机推荐

  1. 最简单的git 用法

    步骤 在机器上ssh-keygen 然后把id_rsa.pub 复制到csdn 的公钥上去. git clone git@code.csdn.net:aca_jingru/zabbix.git 如果这 ...

  2. python -使用Requests库完成Post表单操作

    """ 使用Requests库完成Post表单操作 """ #_*_codingn:utf8 _*_ import requests fro ...

  3. class.getFields和class.getDeclareFields的区别

    class.getFields的定义 返回类提供的public域包括超类的共有变量; 注: 是public,我们平时定义变量一般用的private,如果用getFields是不会获得. class.g ...

  4. 数据分析之Numpy、Matplotlib库

    NumPy(Numerical Python) 是 Python 语言的一个扩展程序库,支持大量的维度数组与矩阵运算,此外也针对数组运算提供大量的数学函数库. 菜鸟教程:https://www.run ...

  5. Jquery学习2---倒计时

    以下代码是mvc4.0代码,其功能是让页面上的数字3,变2,变1 然后跳转页面 @{ ViewBag.Title = "LoginOut"; } <html> < ...

  6. 安卓网络编程学习(1)——java原生网络编程(1)

    写在前面 马上要进行第二轮冲刺,考虑到自己的APP在第一轮冲刺的效果不尽人意,有很多网络方面的小BUG,这里就系统学习一下网络编程,了解来龙去脉,以便更好的对项目进行优化处理. http协议 http ...

  7. hdu3861他的子问题是poj2762二分匹配+Tarjan+有向图拆点 其实就是求DAG的最小覆盖点

    The King’s Problem Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Other ...

  8. Java内存虚拟机理解

        对于Java程序员,在虚拟机自动内存管理机制的帮助下,不需要再为每一个操作写配对的释放资源操作,不容易出现内存泄露和内存溢出问题.加深对Java虚拟机的理解,有助于在发现问题时精准定位问题,排 ...

  9. Closures Basic

    Closures Closures are one of the most powerful features of JavaScript. JavaScript allows for the nes ...

  10. fastclick从接触到丢弃

    fastclick简介 fastclick是一款为了解决移动端300ms点击延迟而诞生的插件. 在移动端,如果对页面没有做任何处理,点击一个元素,触发的事件流程可简单理解为:touch -> 经 ...