前面介绍了事件源(EventSource)和集群(cluster),现在到了讨论CQRS的时候了。CQRS即读写分离模式,由独立的写方程序和读方程序组成,具体原理在以前的博客里介绍过了。akka-typed应该自然支持CQRS模式,最起码本身提供了对写方编程的支持,这点从EventSourcedBehavior 可以知道。akka-typed提供了新的EventSourcedBehavior-Actor,极大方便了对persistentActor的应用开发,但同时也给编程者造成了一些限制。如手工改变状态会更困难了、EventSourcedBehavior不支持多层式的persist,也就是说通过persist某些特定的event然后在event-handler程序里进行状态处理是不可能的了。我这里有个例子,是个购物车应用:当完成支付后需要取个快照(snapshot),下面是这个snapshot的代码:

       snapshotWhen {
(state,evt,seqNr) => CommandHandler.takeSnapshot(state,evt,seqNr)
}
... def takeSnapshot(state: Voucher, evt: Events.Action, lstSeqNr: Long)(implicit pid: PID) = {
if (evt.isInstanceOf[Events.PaymentMade]
|| evt.isInstanceOf[Events.VoidVoucher.type]
|| evt.isInstanceOf[Events.SuspVoucher.type])
if (state.items.isEmpty) {
log.step(s"#${state.header.num} taking snapshot at [$lstSeqNr] ...")
true
} else
false
else
false }

判断event类型是没有问题的,因为正是当前的事件,但另一个条件是购物车必须是清空了的。这个有点为难,因为这个状态要依赖这几个event运算的结果才能确定,也就是下一步,但确定结果又需要对购物车内容进行计算,好像是个死循环。在akka-classic里我们可以在判断了event运算结果后,如果需要改变状态就再persist一个特殊的event,然后在这个event的handler进行状态处理。没办法,EventSourcedBehavior不支持多层persist,只有这样做:

      case PaymentMade(acct, dpt, num, ref,amount) =>
...
writerInternal.lastVoucher = Voucher(vchs, vItems)
endVoucher(Voucher(vchs,vItems),TXNTYPE.sales)
Voucher(vchs.nextVoucher, List())
...

我只能先吧当前状态保存下来、进行结单运算、然后清空购物车,这样snapshot就可以顺利进行了。

好了,akka的读方编程是通过PersistentQuery实现的。reader的作用就是把event从数据库读出来后再恢复成具体的数据格式。我们从reader的调用了解一下这个应用里reader的实现细节:

    val readerShard = writerInternal.optSharding.get
val readerRef = readerShard.entityRefFor(POSReader.EntityKey, s"$pid.shopId:$pid.posId")
readerRef ! Messages.PerformRead(pid.shopid, pid.posid,writerInternal.lastVoucher.header.num,writerInternal.lastVoucher.header.opr,bseq,eseq,txntype,writerInternal.expurl,writerInternal.expacct,writerInternal.exppass)

可以看到这个reader是一个集群分片,sharding-entity。想法是每单完成购买后发个消息给一个entity、这个entity再完成reader功能后自动终止,立即释放出占用的资源。reader-actor的定义如下:

object POSReader extends LogSupport {
val EntityKey: EntityTypeKey[Command] = EntityTypeKey[Command]("POSReader") def apply(nodeAddress: String, trace: Boolean): Behavior[Command] = {
log.stepOn = trace
implicit var pid: PID = PID("","")
Behaviors.supervise(
Behaviors.setup[Command] { ctx =>
Behaviors.withTimers { timer =>
implicit val ec = ctx.executionContext
Behaviors.receiveMessage {
case PerformRead(shopid, posid, vchnum, opr, bseq, eseq, txntype, xurl, xacct, xpass) =>
pid = PID(shopid, posid)
log.step(s"POSReader: PerformRead($shopid,$posid,$vchnum,$opr,$bseq,$eseq,$txntype,$xurl,$xacct,$xpass)")(PID(shopid, posid))
val futReadSaveNExport = for {
txnitems <- ActionReader.readActions(ctx, vchnum, opr, bseq, eseq, trace, nodeAddress, shopid, posid, txntype)
_ <- ExportTxns.exportTxns(xurl, xacct, xpass, vchnum, txntype == Events.TXNTYPE.suspend,
{ if(txntype == Events.TXNTYPE.voidall)
txnitems.map (_.copy(txntype=Events.TXNTYPE.voidall))
else txnitems },
trace)(ctx.system.toClassic, pid)
} yield ()
ctx.pipeToSelf(futReadSaveNExport) {
case Success(_) => {
timer.startSingleTimer(ReaderFinish(shopid, posid, vchnum), readInterval.seconds)
StopReader
}
case Failure(err) =>
log.error(s"POSReader: Error: ${err.getMessage}")
timer.startSingleTimer(ReaderFinish(shopid, posid, vchnum), readInterval.seconds)
StopReader
} Behaviors.same
case StopReader =>
Behaviors.same
case ReaderFinish(shopid, posid, vchnum) =>
Behaviors.stopped(
() => log.step(s"POSReader: {$shopid,$posid} finish reading voucher#$vchnum and stopped")(PID(shopid, posid))
)
}
}
}
).onFailure(SupervisorStrategy.restart)
}

reader就是一个普通的actor。值得注意的是读方程序可能是一个庞大复杂的程序,肯定需要分割成多个模块,所以我们可以按照流程顺序进行模块功能切分:这样下面的模块可能会需要上面模块产生的结果才能继续。记住,在actor中绝对避免阻塞线程,所有的模块都返回Future, 然后用for-yield串起来。上面我们用了ctx.pipeToSelf 在Future运算完成后发送ReaderFinish消息给自己,通知自己停止。

在这个例子里我们把reader任务分成:

1、从数据库读取事件

2、事件重演一次产生状态数据(购物车内容)

3、将形成的购物车内容作为交易单据项目存入数据库

4、向用户提供的restapi输出交易数据

event读取是通过cassandra-persistence-plugin实现的:

    val query =
PersistenceQuery(classicSystem).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier) // issue query to journal
val source: Source[EventEnvelope, NotUsed] =
query.currentEventsByPersistenceId(s"${pid.shopid}:${pid.posid}", startSeq, endSeq) // materialize stream, consuming events
val readActions: Future[List[Any]] = source.runFold(List[Any]()) { (lstAny, evl) => evl.event :: lstAny }

这部分比较简单:定义一个PersistenceQuery,用它产生一个Source,然后run这个Source获取Future[List[Any]]。

重演事件产生交易数据:

    def buildVoucher(actions: List[Any]): List[TxnItem] = {
log.step(s"POSReader: read actions: $actions")
val (voidtxns,onlytxns) = actions.asInstanceOf[Seq[Action]].pickOut(_.isInstanceOf[Voided])
val listOfActions = onlytxns.reverse zip (LazyList from ) //zipWithIndex
listOfActions.foreach { case (txn,idx) =>
txn.asInstanceOf[Action] match {
case Voided(_) =>
case ti@_ =>
curTxnItem = EventHandlers.buildTxnItem(ti.asInstanceOf[Action],vchState).copy(opr=cshr)
if(voidtxns.exists(a => a.asInstanceOf[Voided].seq == idx)) {
curTxnItem = curTxnItem.copy(txntype = TXNTYPE.voided, opr=cshr)
log.step(s"POSReader: voided txnitem: $curTxnItem")
}
val vch = EventHandlers.updateState(ti.asInstanceOf[Action],vchState,vchItems,curTxnItem,true)
vchState = vch.header
vchItems = vch.txnItems
log.step(s"POSReader: built txnitem: ${vchItems.txnitems.head}")
}
}
log.step(s"POSReader: voucher built with state: $vchState, items: ${vchItems.txnitems}")
vchItems.txnitems
}

重演List[Event],产生了List[TxnItem]。

向数据库里写List[TxnItem]:

 def writeTxnsToDB(vchnum: Int, txntype: Int, bseq: Long, eseq: Long, txns: List[TxnItem])(
implicit system: akka.actor.ActorSystem, session: CassandraSession, pid: PID): Future[Seq[TxnItem]] = ???

注意返回结果类型Future[Seq[TxnItem]]。我们用for-yield把这几个动作串起来:

  val txnitems: Future[List[Events.TxnItem]] = for {
lst1 <- readActions //read list from Source
lstTxns <- if (lst1.length < (endSeq -startSeq)) //if imcomplete list read again
readActions
else FastFuture.successful(lst1)
items <- FastFuture.successful( buildVoucher(lstTxns) )
_ <- JournalTxns.writeTxnsToDB(vchnum,txntype,startSeq,endSeq,items)
_ <- session.close(ec)
} yield items

注意返回结果类型Future[Seq[TxnItem]]。我们用for-yield把这几个动作串起来:

  val txnitems: Future[List[Events.TxnItem]] = for {
lst1 <- readActions //read list from Source
lstTxns <- if (lst1.length < (endSeq -startSeq)) //if imcomplete list read again
readActions
else FastFuture.successful(lst1)
items <- FastFuture.successful( buildVoucher(lstTxns) )
_ <- JournalTxns.writeTxnsToDB(vchnum,txntype,startSeq,endSeq,items)
_ <- session.close(ec)
} yield items

注意:这个for返回的Future[List[TxnItem]],是提供给restapi输出功能的。在那里List[TxnItem]会被转换成json作为post的包嵌数据。

现在所有子任务的返回结果类型都是Future了。我们可以再用for来把它们串起来:

             val futReadSaveNExport = for {
txnitems <- ActionReader.readActions(ctx, vchnum, opr, bseq, eseq, trace, nodeAddress, shopid, posid, txntype)
_ <- ExportTxns.exportTxns(xurl, xacct, xpass, vchnum, txntype == Events.TXNTYPE.suspend,
{ if(txntype == Events.TXNTYPE.voidall)
txnitems.map (_.copy(txntype=Events.TXNTYPE.voidall))
else txnitems },
trace)(ctx.system.toClassic, pid)
} yield ()

说到EventSourcedBehavior,因为用了cassandra-plugin,忽然想起配置文件里新旧有很大区别。现在这个application.conf是这样的:

akka {
loglevel = INFO
actor {
provider = cluster
serialization-bindings {
"com.datatech.pos.cloud.CborSerializable" = jackson-cbor
}
}
remote {
artery {
canonical.hostname = "192.168.11.189"
canonical.port =
}
}
cluster {
seed-nodes = [
"akka://cloud-pos-server@192.168.11.189:2551"]
sharding {
passivate-idle-entity-after = m
}
}
# 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 = "poc2g"
journal.keyspace-autocreate = on
journal.tables-autocreate = on
snapshot.keyspace = "poc2g_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"
}

akka.persitence.cassandra段落里可以定义keyspace名称,这样新旧版本应用可以共用一个cassandra,同时在线。

akka-typed(8) - CQRS读写分离模式的更多相关文章

  1. CQRS读写职责分离模式(Command and Query Responsibility Segregation (CQRS) Pattern)

    此文翻译自msdn,侵删. 原文地址:https://msdn.microsoft.com/en-us/library/dn568103.aspx 通过使用不同的接口来分离读和写操作,这种模式最大化了 ...

  2. 使用读写分离模式扩展 Grafana Loki

    转载自:https://mp.weixin.qq.com/s?__biz=MzU4MjQ0MTU4Ng==&mid=2247500127&idx=1&sn=995987d558 ...

  3. mycat结合双主复制实现读写分离模式

    简介:应用程序仅需要连接mycat,后端服务器的读写分离由mycat进行控制,后端服务器数据的同步由MySQL主从同步进行控制. 本次实验环境架构图 服务器主机规划 主机名 IP  功能 备注 lin ...

  4. 危险!水很深,让叔来 —— 谈谈命令查询权责分离模式(CQRS)

    多年以前,那时我正年轻,做技术如鱼得水,甚至一度希望自己能当一辈子的一线程序员. 但是我又有两个小愿望想要达成:一个是想多挣点钱:另一个就是对项目的技术栈和架构选型能多有点主动权. 多挣点钱是因为当时 ...

  5. Mycat主从模式下的读写分离与自动切换

    1. 机器环境 192.168.2.136 mycat1 192.168.2.134 mydb1 192.168.2.135 mydb2 2在mysql1.mysql2上安装mysql 更改root用 ...

  6. 数据源管理 | 主从库动态路由,AOP模式读写分离

    本文源码:GitHub·点这里 || GitEE·点这里 一.多数据源应用 1.基础描述 在相对复杂的应用服务中,配置多个数据源是常见现象,例如常见的:配置主从数据库用来写数据,再配置一个从库读数据, ...

  7. mysql读写分离——中间件ProxySQL的简介与配置

    mysql实现读写分离的方式 mysql 实现读写分离的方式有以下几种: 程序修改mysql操作,直接和数据库通信,简单快捷的读写分离和随机的方式实现的负载均衡,权限独立分配,需要开发人员协助. am ...

  8. MySQL中间件之ProxySQL(10):读写分离方法论

    返回ProxySQL系列文章:http://www.cnblogs.com/f-ck-need-u/p/7586194.html 1.不同类型的读写分离 数据库中间件最基本的功能就是实现读写分离,Pr ...

  9. 读写分离,读写分离死锁解决方案,事务发布死锁解决方案,发布订阅死锁解决方案|事务(进程 ID *)与另一个进程被死锁在 锁 资源上,并且已被选作死锁牺牲品。请重新运行该事务

    前言:         由于网站访问压力的问题,综合分析各种因素后结合实际情况,采用数据库读写分离模式来解决当前问题.实际方案中采用“事务发布”模式实现主数据库和只读数据库的同步,其中: 发布服务器1 ...

随机推荐

  1. Rocket - debug - TLDebugModuleInner - DMI Register Control and Status

    https://mp.weixin.qq.com/s/tI41wu0xaIQ5PRq6F82tNw 简单介绍TLDebugModuleInner中生成DMI控制和状态寄存器使用到的状态. 1. abs ...

  2. Rocket - decode - SimplifyDC

    https://mp.weixin.qq.com/s/4uWqBRrMVG6FlnBKmw8U-w   介绍SimplifyDC如何简化解码逻辑.     1. 使用   ​​   简化从mint和m ...

  3. MyBatis特性详解

    缓存简介 一般我们在系统中使用缓存技术是为了提升数据查询的效率.当我们从数据库中查询到一批数据后将其放入到混存中(简单理解就是一块内存区域),下次再查询相同数据的时候就直接从缓存中获取数据就行了. 这 ...

  4. Java实现 LeetCode 650 只有两个键的键盘(递归 || 数学)

    650. 只有两个键的键盘 最初在一个记事本上只有一个字符 'A'.你每次可以对这个记事本进行两种操作: Copy All (复制全部) : 你可以复制这个记事本中的所有字符(部分的复制是不允许的). ...

  5. Java实现 LeetCode 330 按要求补齐数组

    330. 按要求补齐数组 给定一个已排序的正整数数组 nums,和一个正整数 n .从 [1, n] 区间内选取任意个数字补充到 nums 中,使得 [1, n] 区间内的任何数字都可以用 nums ...

  6. Java实现 LeetCode 35 搜索插入位置

    35. 搜索插入位置 给定一个排序数组和一个目标值,在数组中找到目标值,并返回其索引.如果目标值不存在于数组中,返回它将会被按顺序插入的位置. 你可以假设数组中无重复元素. 示例 1: 输入: [1, ...

  7. Android studio环境配置(运行报错)

    报错的种类有很多,下面的方法能解决大多数: 所有路径不能用中文,不能有空格,逗号句号也不能用中文,项目文件路径也不行 首先要配置Java环境,这个就不多说了, 这里有以下JAVA_HOME的配置,下图 ...

  8. java实现排他平方数

    题目标题: 排它平方数 小明正看着 203879 这个数字发呆. 原来,203879 * 203879 = 41566646641 这有什么神奇呢?仔细观察,203879 是个6位数,并且它的每个数位 ...

  9. 使用vw进行移动端适配(nuxt项目)

    基于nuxt 2.0.0 一.安装postcss-px-to-viewport npm安装 npm install postcss-px-to-viewport --save-dev 或 yarn安装 ...

  10. test tt=0 <=>test(0)

    class test{ int mvalue; public: test(int i){ cout << "test(int i) =" << i < ...