前面我们全面介绍了在akka-cluster环境下实现的CQRS写端write-side。简单来说就是把发生事件描述作为对象严格按发生时间顺序写入数据库。这些事件对象一般是按照二进制binary方式如blob存入数据库的。cassandra-plugin的表结构如下:

CREATE KEYSPACE IF NOT EXISTS akka
WITH REPLICATION = { 'class' : 'SimpleStrategy','replication_factor':1 }; CREATE TABLE IF NOT EXISTS akka.messages (
used boolean static,
persistence_id text,
partition_nr bigint,
sequence_nr bigint,
timestamp timeuuid,
timebucket text,
writer_uuid text,
ser_id int,
ser_manifest text,
event_manifest text,
event blob,
meta_ser_id int,
meta_ser_manifest text,
meta blob,
message blob,
tags set<text>,
PRIMARY KEY ((persistence_id, partition_nr), sequence_nr, timestamp, timebucket))
WITH gc_grace_seconds =864000
AND compaction = {
'class' : 'SizeTieredCompactionStrategy',
'enabled' : true,
'tombstone_compaction_interval' : 86400,
'tombstone_threshold' : 0.2,
'unchecked_tombstone_compaction' : false,
'bucket_high' : 1.5,
'bucket_low' : 0.5,
'max_threshold' : 32,
'min_threshold' : 4,
'min_sstable_size' : 50
};

事件对象是存放在event里的,是个blob类型字段。下面是个典型的写动作示范:

  val receiveCommand: Receive = {
case Cmd(data) =>
persist(ActionGo) { event =>
updateState(event)
}
}

这些事件描述的存写即写这个ActionGo时不会影响到实际业务数据状态。真正发生作用,改变当前业务数据状态的是在读端read-side。也就是说在另一个线程里有个程序也按时间顺序把这些二进制格式的对象读出来、恢复成某种结构如ActionGo类型、然后按照结构内的操作指令对业务数据进行实际操作处理,这时才会产生对业务数据的影响。做个假设:如果这些事件不会依赖时间顺序的话是不是可以偷偷懒直接用一种pub/sub模式把reader放在订阅subscriber端,如下:

//写端
import DistributedPubSubMediator.Publish
val mediator = DistributedPubSub(context.system).mediator val receiveCommand: Receive = {
case Cmd(data) =>
persist(DataUpdated) { event =>
updateState(event)
mediator ! Publish(persistentId, event,sendOneMessageToEachGroup = true)
}
} //读端
val mediator = DistributedPubSub(context.system).mediator
mediator ! Subscribe(persistentId, self)
def receive = {
case DataUpdated: Event ⇒
updateDataTables()
}

这种pub/sub模式的特点是消息收发双方耦合度非常松散,但同时也存在订阅方sub即reader十分难以控制的问题,而且可以肯定的是订阅到达消息无法保证是按发出时间顺序接收的,我们无法控制akka传递消息的过程。因为业务逻辑中一个动作的发生时间顺序往往会对周围业务数据产生不同的影响,所以现在只能考虑事件源event-sourcing这种模式了。es方式的CQRS是通过数据库表作为读写间隔实现写端程序和读端程序的分离。写端只管往数据库写数据操作指令,读端从同一数据库位置读出指令进行实质的数据处理操作,所以读写过程中会产生一定的延迟,读端需要不断从数据库抽取pull事件。而具体pull的时段间隔如何设定也是一个比较棘手的问题。无论如何,akka提供了Persistence-Query作为一种CQRS读端工具。我们先从一个简单的cassandra-persistence-query用例开始:

// obtain read journal by plugin id
val readJournal =
PersistenceQuery(system).readJournalFor[CassandraReadJournal](CassandraReadJournal.Identifier) // issue query to journal
val source: Source[EventEnvelope, NotUsed] =
readJournal.eventsByPersistenceId("user-1337", 0, Long.MaxValue) // materialize stream, consuming events
implicit val mat = ActorMaterializer()
source.runForeach { pack =>
updateDatabase(pack.event)
}

eventsByPersistenceId(...)构建了一个akka-stream的Source[EventEnvelope,_]。这个EventEnvelope类定义如下:

/**
* Event wrapper adding meta data for the events in the result stream of
* [[akka.persistence.query.scaladsl.EventsByTagQuery]] query, or similar queries.
*/
final case class EventEnvelope(
offset: Offset,
persistenceId: String,
sequenceNr: Long,
event: Any)

上面这个event字段就是从数据库读取的事件对象。EventEnvelope是以流元素的形式从数据库中提出。eventsByPersistenceId(...)启动了一个数据流,然后akka-persistence-query会按refresh-interval时间间隔重复运算这个流stream。refresh-interval可以在配置文件中设置,如下面的cassandra-plugin配置:

cassandra-query-journal {
# Implementation class of the Cassandra ReadJournalProvider
class = "akka.persistence.cassandra.query.CassandraReadJournalProvider" # Absolute path to the write journal plugin configuration section
write-plugin = "cassandra-journal" # New events are retrieved (polled) with this interval.
refresh-interval = 3s ...
}

以上描述的是一种接近实时的读取模式。一般来讲,为了实现高效、安全的事件存写,我们会尽量简化事件结构,这样就会高概率出现一个业务操作单位需要多个事件来描述,那么如果在完成一项业务操作单元的所有事件存写后才开始读端的动作不就简单多了吗?而且还比较容易控制。虽然这样会造成某种延迟,但如果以业务操作为衡量单位,这种延迟应该是很正常的,可以接受的。现在每当完成一项业务的所有事件存写后在读端一次性成批把事件读出来然后进行实质的数据操作,应当可取。akka-persistence-query提供了下面这个函数:

  /**
   * Same type of query as `eventsByPersistenceId` but the event stream
   * is completed immediately when it reaches the end of the "result set". Events that are
   * stored after the query is completed are not included in the event stream.
   */
  override def currentEventsByPersistenceId(
      persistenceId: String,
      fromSequenceNr: Long,
      toSequenceNr: Long): Source[EventEnvelope, NotUsed] = ...

我们可以run这个stream把数据读入一个集合里,然后可以在任何一个线程里用这个集合演算业务逻辑(如我们前面提到的写端状态转变维护过程),可以用router/routee模式来实现一种在集群节点中负载均衡式的分配reader-actor作业节点。

下一篇准备对应前面的CQRS Writer Actor 示范里的akka-cluster-pos进行rCQRS-Reader-Actor示范。

Akka-CQRS(6)- read-side的更多相关文章

  1. Akka系列(三):监管与容错

    前言...... Akka作为一种成熟的生产环境并发解决方案,必须拥有一套完善的错误异常处理机制,本文主要讲讲Akka中的监管和容错. 监管 看过我上篇文章的同学应该对Actor系统的工作流程有了一定 ...

  2. Akka系列(二):Akka中的Actor系统

    前言......... Actor模型作为Akka中最核心的概念,所以Actor在Akka中的组织结构是至关重要,本文主要介绍Akka中Actor系统. 1.Actor系统 Actor作为一种封装状态 ...

  3. Akka系列(八):Akka persistence设计理念之CQRS

    前言........ 这一篇文章主要是讲解Akka persistence的核心设计理念,也是CQRS(Command Query Responsibility Segregation)架构设计的典型 ...

  4. CQRS学习——最小单元的Cqrs(CommandEvent)[其一]

    [说明:博主采用边写边思考的方式完成这一系列的博客,所以代码以附件为准,文中代码仅为了说明.] 结构 在学习和实现CQRS的过程中,首要参考的项目是这个[http://www.cnblogs.com/ ...

  5. Akka系列(九):Akka分布式之Akka Remote

    前言.... Akka作为一个天生用于构建分布式应用的工具,当然提供了用于分布式组件即Akka Remote,那么我们就来看看如何用Akka Remote以及Akka Serialization来构建 ...

  6. Akka系列(六):Actor解决了什么问题?

    前言..... 文档来源于  : What problems does the actor model solve? Actor解决了什么问题? Akka使用Actor模型来克服传统面向对象编程模型的 ...

  7. akka开发(一)HelloWorld

    package com.hfi.helloakka; import akka.actor.ActorRef; import akka.actor.Props; import akka.actor.Un ...

  8. Akka系列(十):Akka集群之Akka Cluster

    前言........... 上一篇文章我们讲了Akka Remote,理解了Akka中的远程通信,其实Akka Cluster可以看成Akka Remote的扩展,由原来的两点变成由多点组成的通信网络 ...

  9. Akka系列(七):Actor持久化之Akka persistence

    前言.......... 我们在使用Akka时,会经常遇到一些存储Actor内部状态的场景,在系统正常运行的情况下,我们不需要担心什么,但是当系统出错,比如Actor错误需要重启,或者内存溢出,亦或者 ...

  10. Akka系列(四):Akka中的共享内存模型

    前言...... 通过前几篇的学习,相信大家对Akka应该有所了解了,都说解决并发哪家强,JVM上面找Akka,那么Akka到底在解决并发问题上帮我们做了什么呢? 共享内存 众所周知,在处理并发问题上 ...

随机推荐

  1. abap 常用TCODE

    ABAP: 通过查询表TSTC或者TSTCT:SAP系统将所有的事务代码都存储在这个表中,包括字开发的Y*和Z* TCODE 事务代码功能描述 CG3Y 下载服务器上文件 CG3Z upload fi ...

  2. vue 打包后本地先自己启动服务 anywhere 非常好用

    :)nodejs服务器Anywhere Anywhere是一个随启随用的静态服务器,它可以随时随地将你的当前目录变成一个静态文件服务器的根目录. 一,安装node 在nodejs官网下载,安装后打开c ...

  3. 十五、Collections.sort(<T>, new Comparator<T>() {})针对字符串排序

    1.排序对象全是字母组成,可以根据ASCII编码表排序 package com.abcd; public class Person{ private String name; private int ...

  4. Unity 角色移动贴墙行走

    直接贴上代码,旋转角色角度检测碰撞 Vector2 v2Normal = new Vector2(normal.x, normal.y); float fAngle = Vector2.SignedA ...

  5. 2018年 js 简易控制滚动条滚动的简单方法

    首先是es2015 的新api Element.scrollIntoView() // 滚动到最上方 等同于 dom.scrollIntoView(true) Element.scrollIntoVi ...

  6. 分离式lnmp部署

    分离式lnmp简介 lnmp和lamp一样,是一种常用的web环境, 在实际环境中,lnmp中的三个服务常常为了更好的性能而分在三台主机上安装. 本篇内只介绍nginx和php-fpm+mysql分离 ...

  7. (python)编程小练习

    1.将一串字符串反向输出,如将"abcd"变成“dcba” str1="abcd" print str1[::-1] 2.判断是否为回文——判断用户输入的字符串 ...

  8. py3.0第四天 函数,生成器迭代器等

    1.列表生成式,迭代器&生成器 孩子,我现在有个需求,看列表[0, 1, 2, 3, 4, 5, 6, 7, 8, 9],我要求你把列表里的每个值加1,你怎么实现?你可能会想到2种方式 > ...

  9. 运用PIL库 用来美白,磨皮,瘦脸等操作!

    1.安装pillow库: 在cmd下,输入简单的命令: pip install pillow  即可安装pillow库. 2.PIL库的简介: 1. PIL库主要有2个方面的功能: (1) 图像归档: ...

  10. WebAPI之DOM和BOM

    API是什么? Application Programming Interface:应用程序编程接口,是一些预先定义的函数,通俗的理解就是一些方法. WebAPI是什么? 浏览器提供的一套操作浏览器功 ...