前面我们全面介绍了在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. Spring 事物Transaction

    日常开发中Spring 为我们提供了两种事物的定义方式 XML 配置 方式 :这种方式配置起来比较麻烦,但后期比较好进行维护 注解方式:配置起来比较方便,也是日常开发常用的: 我们这里进行第二种注解的 ...

  2. css去掉滚动条

    .main-layout-side::-webkit-scrollbar { display: none; } 主要代码: ::-webkit-scrollbar {display: none;}

  3. Chapter_4_JAVA作业

    一.类的封装,继承与多态 1.课前预习 1.1 举列现实生活中的封装,以及简述在程序中什么是属性的封装? 1.1.1 将东西捆绑在一起,如集成芯片:高压电线等等 1.1.2 封装就是将属性私有化,提供 ...

  4. 使用Druid作为SpringBoot项目数据源(添加监控)

    Druid是一个关系型数据库连接池,它是阿里巴巴的一个开源项目.Druid支持所有JDBC兼容数据库,包括了Oracle.MySQL.PostgreSQL.SQL Server.H2等.Druid在监 ...

  5. 团队项目Alpha冲刺阶段之学习总结

    线性布局 线性布局是程序中最常见的种布局方式,可以分为水平线性布局和重直线性布局两种,通过Android:orientation属性可以设置线性布局的方向.线性布局的特点是各个子元法彼此连接,中间不留 ...

  6. Java 调用执行其他语言的程序

    以 Java 调用 Python 为例 1. 使用 Runtime 类 该方式简单,但是增加了 Java 对python 的依赖,需要事先安装python,及python程序依赖的第三方库 Runti ...

  7. 腾讯开源的Paxos库PhxPaxos代码解读---Prepare阶段(一)

    简单的画了一下PhxPaxos在Prepare阶段的逻辑,主要是正常的逻辑,异常逻辑和超时后面再写了; 熟悉PhxPaxos代码最好的方法是编译运行sample目录下的三个例子,编译方法在另一篇博客已 ...

  8. 旅游类App的原型制作分享-Klook

    Klook是一款旅游类App,它能探索和预订惊人的旅行活动.在世界各地以最优惠的价格畅玩. 这款原型中,用到了Mockplus的两种滚动方式,一种是把手机外壳拉长,另一种是使用滚动区组件,其中,滚动区 ...

  9. OO前三次作业分析

    一,第一次作业分析 度量分析: 第一次的oo作业按照常理来说是不应该有这么多的圈复杂度,但是由于第一次写的时候,完全不了解java的相关知识,按照c语言的方式来写,完全的根据指导书的逻辑,先写好了正确 ...

  10. WCF系列_WCF常用绑定选择

    一.五种常用绑定常用绑定的传输协议以及编码格式 名称 传输协议 编码格式 互操作性 BasicHttpBinding HTTP/HTTPS Text,MTOM Yes NetTcpBinding TC ...