cqrs_编写基于事件的CQRS读取模型

cqrs

cqrs

关于事件源和CQRS的讨论似乎通常集中在CQRS上下文中的整体系统架构或领域驱动设计的各种形式。 但是,尽管也有一些有趣的考虑,但读取模型经常被忽略。 在本文中,我们将介绍通过使用事件流来填充视图模型的示例实现。

es_cqrs_projection_funnel_300_2

总览

读取模型的想法非常简单。 您获取事件日志,使用适当的功能在最初为空的数据模型上应用(重放)所有事件,然后获得填充的模型。 代码如下所示:

List<Event> events = getEvents();
Model model = Model.empty();
for (Event event : events) {
    apply(model, event);
}

通过函数式编程,我们可以使此过程更短:

Model m = reduce(getEvents(),
                 Model.empty(),
                 (m, e) -> apply(m, e));

这就是本质。 请注意,这只是抽象的轮廓,实际的实现可能会有所不同,包括缓冲,批处理(或流式传输),持久性等。

申请活动

应用事件的实际Java代码可能类似于以下内容:

EventProcessingResult processEvents() {
    if (getState().isRunning()) {
        int batchSize = getEventsPerIteration();
        List<Event> events = eventStore.getEventsForAllStreams(getLastEventId(),
                                                               batchSize);
        if (events.isEmpty()) {
            return NO_EVENTS_TO_PROCESS;
        } else {
            return processEvents(events);
        }
    } else {
        return NOT_RUNNING;
    }
}

EventProcessingResult processEvents(List<Event> events) {
    try {
        for (Event event : events) {
            dispatchEvent(event);
        }
        return SUCCESS;
    } catch (RuntimeException e) {
        return FAILURE;
    }
}

总而言之,这确实非常简单明了。 在处理单个事件和整个批处理之前和之后,可以使用钩子来增强它。 这样的钩子可以用来:

  • 实施交易,
  • 插入监控,
  • 实施错误处理,
  • 根据速度计算批次大小,
  • 执行任意操作,例如设置某些内容或每批重新计算一次。

最后一个有趣的部分是dispatchEvent方法。 除了遍历类型层次结构,错误处理并使其全部可选之外,它还可以归结为:

void dispatchEvent(Event e) {
    Method handler = projector.getClass().findMethod("on", e.getClass());
    handler.invoke(projector, e);
}

换句话说,对于每种事件类型(如OrderCreated ),我们在projector对象上寻找一个名为on的公共方法on该方法采用一个匹配类型的单个参数。

以上所有都是引擎的一部分,是支持许多视图模型的基础架构。 实际上,实现投影所需的全部工作就是为投影仪提供有趣的事件类型的处理程序。 所有其他事件都将被忽略。

它可能看起来像这样:

public class OrderProjector {
    @Inject
    private OrderDao orders;

    public void on(OrderCreated e) {
        orders.save(new Order(e.getOrderNumber()));
    }

    public void on(OrderApproved e) {
        Order o = orders.find(e.getOrderNumber());
        o.setApproved(true);
    }
}

投影线

让我们讨论一下多线程。 共享的可变状态会立即带来许多问题,应尽可能避免。 处理它的一种方法是首先没有并发性,例如通过限制对单个线程的写入。 在大多数情况下,单线程写入器与ACID事务相结合足以应付写入负载。 (读取/查询负载可能很重,并且使用许多线程–此处所有详细信息仅与写入有关。)

从查询事件存储到更新视图模型数据库,该线程负责将事件应用于读取的模型。 通常,它只是从商店中加载一批事件并应用它们。 只要有更多事件要处理,它就会继续,并在捕获到事件后进入睡眠状态。 在一定时间后或事件存储通知新事件时,它将唤醒。

我们还可以控制此线程的生命周期。 例如,我们提供了一种以编程方式暂停和恢复每个投影线程的方法,甚至可以在管理GUI中公开。

projection_admin

推还是拉?

使用数据库支持的事件存储,可以很容易地重复查询新事件。 这是模型。 不幸的是,这还意味着您可能最终会轮询过多并产生不必要的负载,或者轮询频率太低,从而可能需要更长的时间才能将更改传播到视图模型。

这就是为什么除了轮询事件存储之外,最好引入通知,以在保存新事件后立即唤醒读取的模型。 这有效地成为了具有最小延迟和负载的推送模型。 我们发现JGroups是完成这项工作的非常好的工具–它支持多种协议,并且易于设置,与成熟的消息队列相比,所涉及的麻烦要少得多。

通知可能包含也可能不包含实际事件。

在后一种(和更简单的)设计中,它们仅传播已保存新事件的信息及其顺序ID(以便所有预测都可以估算出它们背后的数量)。 唤醒后,执行程序可以从查询事件存储开始沿其正常路径继续。

为什么? 因为处理来自单一来源的事件比较容易,但是更重要的是,由于数据库支持的事件存储可以轻松地保证排序,并且消息丢失或重复不存在任何问题。 鉴于我们正在按主键顺序读取单个表,而且大多数情况下数据仍在RAM高速缓存中,因此查询数据库的速度非常快。 瓶颈在于投影线程正在更新其读取模型数据库。

但是,将事件数据放入通知中没有任何障碍(可能是出于大小或网络流量方面的考虑)。 这可能会减少事件存储上的负载并节省一些数据库往返时间。 投影仪将需要维护一个缓冲区,并在需要时退回查询事件存储。 或者系统可以使用更可靠的消息队列。

重新开始投影

除了暂停/恢复之外,上面的屏幕快照还显示了另一项操作:重新启动。 从外观上看是无害的,这是一个非常不错且功能强大的功能。

由于视图模型是完全从事件日志派生的,因此可以随时将其丢弃(从某个初始状态/足够旧的快照开始)并重新创建。 在事件日志中,数据是安全的,这是事实的最终来源。

当有关视图的任何内容发生更改时,此功能很有用:添加了字段或表,修复了错误,计算出的内容有所不同。 当发生这种情况时,通常从一开始就更容易(或需要),而不是例如实施大量SQL迁移脚本。

甚至可以实现完全自动化,以便在系统启动并检测到DB模式与相应的Java模型不匹配时,它可以自动重新创建模式并重新处理事件日志。 就像使用Hibernate create-drop策略运行一样,不同之处在于它不会丢失数据。

性能

该解决方案在性能方面可能看起来非常有限。

单线程作家可能引起人们的注意。 实际上,单个线程通常足够快,可以轻松地跟上负载。 并发不仅更难以实现和维护,而且还引入了竞争。 读取(查询)可以是多线程的,并且易于扩展。

通过拥有多个读取模型,例如从管理和“交易”数据中分离分析,我们也获得了很多收益。 每个模型都是单线程的(用于编写),但是多个模型并行使用事件。 最后,可以将解决方案修改为使用分片或某种fork-join处理。

另一个有趣的观点是从头开始重新启动投影

一个好的解决方案是类似于kappa体系结构

  • 保持过时的预测正常运行并回答所有查询。
  • 开始一个新的投影,例如到另一个数据库。 只要让它处理事件,就不要指向任何流量。
  • 当新的预测赶上时,请重定向流量并关闭旧的预测。

在很小的情况下,尤其是对于开发而言,甚至可以在同一情况下在线重新启动。 它取决于以下问题的答案:重新处理所有事件需要多长时间? 将这种预测陈旧30分钟是否可以接受? 我们可以在没有人使用该系统的夜晚或周末进行部署吗? 我们需要重播所有历史吗?

这里要考虑的另一个因素是持久性。 如果瓶颈太大,无法进一步优化,请考虑使用内存视图模型。

加起来

本质上,这就是实现使用事件存储区的读取模型所需要的全部。 有了线性事件存储并在单个线程中处理所有内容,它变得非常简单。 如此之多,最后实际上只是一个循环,实现了开头所示的减少。

在以后的文章中,我将更深入地探讨实施预测的实际问题。

翻译自: https://www.javacodegeeks.com/2015/09/writing-an-event-sourced-cqrs-read-model.html

cqrs

CQRS(Command Query Responsibility Segration)架构,大家应该不会陌生了。简单的说,就是一个系统,从架构上把它拆分为两部分:命令处理(写请求)+查询处理(读请求)。然后读写两边可以用不同的架构实现,以实现CQ两端(即Command Side,简称C端;Query Side,简称Q端)的分别优化。CQRS作为一个读写分离思想的架构,在数据存储方面,没有做过多的约束。所以,我觉得CQRS可以有不同层次的实现,比如: 1.CQ两端数据库共享,CQ两端只是在上层代码上分离;这种做法,带来的好处是可以让我们的代码读写分离,更好维护,且没有CQ两端的数据一致性问题,因为是共享一个数据库的。我个人认为,这种架构很实用,既兼顾了数据的强一致性,又能让代码好维护。 2.CQ两端数据库和上层代码都分离,然后Q的数据由C端同步过来,一般是通过Domain Event进行同步。同步方式有两种,同步或异步,如果需要CQ两端的强一致性,则需要用同步;如果能接受CQ两端数据的最终一致性,则可以使用异步。采用这种方式的架构,个人觉得,C端应该采用Event Sourcing(简称ES)模式才有意义,否则就是自己给自己找麻烦。因为这样做你会发现会出现冗余数据,同样的数据,在C端的db中有,而在Q端的db中也有。和上面第一种做法相比,我想不到什么好处。而采用ES,则所有C端的最新数据全部用Domain Event表达即可;而要查询显示用的数据,则从Q端的ReadDB(关系型数据库)查询即可。
第1章我们的领域: 会议管理系统1 1.1Contoso公司简介1 1.2谁与我们同行2 1.3Contoso会议管理系统3 1.3.1系统概览3 1.3.2非功能性需求4 1.4开始我们的旅程5 1.5更多信息5 第2章领域分解——站点规划6 2.1本章术语定义6 2.2会议管理系统里面的有界上下文7 2.2.1订单和注册有界上下文7 2.2.2会议管理有界上下文7 2.2.3支付有界上下文8 2.2.4不包括在内的有界上下文8 2.2.5Contoso会议管理系统的上下文路线图9 2.3为什么选择这些有界上下文10 2.4更多信息11 第3章订单和注册有界上下文12 3.1订单和注册有界上下文简介12 3.2本章术语定义13 3.3领域定义(普适语言)14 3.4订单创建的需求分析16 3.5系统架构17探索CQRS事件源目录3.6模式和概念17 3.6.1系统验证21 3.6.2交易边界22 3.6.3并发处理22 3.6.4Aggregates和Aggregate Roots22 3.7实现细节23 3.7.1高层架构23 3.7.2写者模型28 3.7.3使用Windows Azure服务总线37 3.8对测试的影响44 3.9本章小结47 3.10更多信息47 第4章扩展和改进订单和注册有界上下文48 4.1修改有界上下文48 4.1.1本章术语定义49 4.1.2用户需求49 4.1.3系统架构49 4.2模式和概念51 4.2.1记录定位器51 4.2.2读者端查询51 4.2.3向读者端提供部分履行的订单信息54 4.2.4CQRS命令验证55 4.2.5倒计时定时器和读者模型56 4.3实现细节56 4.3.1订单访问码(记录定位器)57 4.3.2倒计时定时器58 4.3.3使用ASP.NET MVC验证60 4.3.4将改动推送到读者端62 4.3.5重构SeatsAvailability aggregate66 4.4对测试的影响68 4.4.1接受测试和领域专家68 4.4.2使用SpecFlow功能来定义接受测试68 4.4.3通过测试来帮助开发人员理解消息流75 4.5代码理解的旅程: 痛苦、释放和学习的故事77 4.5.1测试很重要77 4.5.2领域测试78 4.5.3硬币的另外一面80 4.6本章小结83 4.7更多信息84 第5章准备V1发布85 5.1Contoso会议管理系统的V1发布版85 5.1.1本章术语定义85 5.1.2用户需求86 5.1.3系统架构87 5.2模式和概念91 5.2.1事件源91 5.2.2基于任务的用户界面92 5.2.3有界上下文之间的集成95 5.2.4分布式交易和事件源98 5.2.5自治与集权99 5.2.6读者端的实现方法100 5.2.7最终一致性100 5.3实现细节101 5.3.1会议管理有界上下文101 5.3.2支付有界上下文102 5.3.3事件源105 5.3.4基于Windows Azure表格的事件库111 5.3.5订单总价计算114 5.4对测试的影响114 5.4.1时序问题114 5.4.2引入领域专家115 5.5本章小结115 5.6更多信息115 第6章系统版本控制116 6.1本章术语定义116 6.1.1用户需求116 6.1.2系统架构117 6.2模式和概念118 6.2.1修改事件定义118 6.2.2确保消息的自洽性119 6.2.3集成事件的保存121 6.2.4消息排序122 6.3实现细节123 6.3.1对零成本订单的支持123 6.3.2显示剩余座位数127 6.3.3删除重复命令130 6.3.4确保消息排序131 6.3.5保存会议管理有界上下文的事件135 6.3.6从V1版本迁移到V2版本139 6.4对测试的影响140 6.4.1重访SpecFlow140 6.4.2在迁移过程中发现错误143 6.5本章小结143 6.6更多信息144 第7章加入弹性和优化性能145 7.1本章术语定义145 7.2系统架构145 7.3加入弹性147 7.3.1增加事件重复处理时的弹性148 7.3.2确保命令的发送148 7.4优化性能148 7.4.1优化前的用户界面流程149 7.4.2用户界面优化150 7.4.3基础设施优化151 7.5无停机迁移158 7.6实现细节159 7.6.1改进RegistrationProcessManager类160 7.6.2用户界面流程优化165 7.6.3消息的异步接收、处理和发送170 7.6.4在流程内部对命令进行同步处理171 7.6.5使用备忘录模式来实现快照173 7.6.6对事件进行并行发布175 7.6.7在订购服务里面对消息进行过滤176 7.6.8为SeatsAvailability aggregate创建专门的SessionSubscriptionReceiver 实例177 7.6.9缓存读者模型数据179 7.6.10使用多个议题来划分服务总线180 7.6.11其他的优化和强化措施181 7.7对测试的影响184 7.7.1集成测试185 7.7.2用户界面测试185 7.8本章小结185 7.9更多信息185 第8章尾声: 经验教训186 8.1我们学到了什么186 8.1.1性能很重要186 8.1.2实现消息驱动并不简单187 8.1.3云平台的挑战187 8.1.4不同的CQRS188 8.1.5事件源和交易日志记录190 8.1.6引入领域专家190 8.1.7什么时候该使用CQRS190 8.2如果重新来过,我们会做的有什么不同191 8.2.1以牢靠的消息和保存基础设施为起点191 8.2.2更好地利用基础设施的能力191 8.2.3采纳更加系统化的方法来实现流程管理器192 8.2.4对应用程序实施不同的划分192 8.2.5以不同方式组织项目团队192 8.2.6对领域和有界上下文的CQRS适用性进行评估192 8.2.7为性能进行规划192 8.2.8重新考虑用户界面193 8.2.9探索事件源的其他用处193 8.2.10探索有界上下文的集成问题193 8.3更多信息194
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值