70+专家分享实战经验,2024年度AI最佳实践都在AICon北京 了解详情
写点什么

使用聚合、事件溯源和 CQRS 开发事务型微服务(第二部分)

  • 2017-02-12
  • 本文字数:6225 字

    阅读完需:约 20 分钟

本文要点

  • 事件溯源技术用于可靠地更新状态并发布事件,它突破了其它解决方案的局限。
  • 在事件驱动架构中使用事件溯源,该设计理念可很好地与微服务架构匹配。
  • 通过组合特定时间点上的所有事件做聚合查询时,使用快照可以改进性能。
  • 事件溯源对查询提出了挑战,借助于 CQRS 指南和物化视图可以克服这些挑战。
  • 事件溯源和 CQRS 无需任何特殊的工具或软件,有很多已有框架在一定程度上补全了底层功能。

第一部分中,我们介绍了领域模型、事务、查询与功能分解之间的相互抵触是我们使用微服务架构的最主要障碍。前文所给出解决方法是将每个服务的业务逻辑实现为一系列DDD 聚合,每个事务更新或创建一个独立的聚合,使用事件维护聚合(还有服务)间的数据一致性。

在第二部分中,我们将介绍聚合的自动更新和事件发布,这是使用事件的主要挑战所在。我们将给出通过事件溯源解决这类问题的方法。事件聚合用于业务逻辑的设计和持久化,是一种以事件为中心的方法。然后我们会介绍微服务是如何让查询变得难以实现的,并给出一种称为命令查询职责分离(CQRS,Command Query Responsibility Segregation)的方法。CQRS 可以实现可扩展且高效的查询。

可靠的状态更新和事件发布

使用事件维护聚合间的一致性从表面上看是非常直接的方法,因为服务在创建或更新了数据库中的聚合时只是发布了一个事件。但是这里存在一个问题,就是必须要原子地完成数据更新并发布事件。否则如果一个服务在更新数据库之后还没来得及发布事件就崩溃了,那么系统将依然会保持不一致的状态。传统的解决方案是使用分布式事务,分布式事务涉及了数据库和消息代理。但是正如我们在第一部分中所介绍的那样,两阶段提交并非一个有效的方法。

有一些方法可以不使用两阶段提交解决这个问题。图1 显示了其中的一种解决方法。该方法让应用通过将事件发布到 Apache Kafka 这样的消息代理执行更新,由订阅了消息代理的消息消费者对数据库做最终更新。该方法能保证了数据库更新和事件发布。但是缺点在于该方法实现了一种更为复杂的一致性模型,应用不能立刻读取自己所写入的数据。

图 1 通过发布到消息代理来更新数据库。

图 2 给出了另一种解决方法。该方法让应用实时跟踪数据库事务日志(也称为提交日志),将日志中记录的每次变更转化为事件,并将事件发布到消息代理。该方法的主要优点在于无需任何对应用做更改,缺点在于难以反向工程成高层的业务事件,这些事件虽然与数据库更新有关,不过将底层的变更转化成表中的数据行并不容易。

(点击放大图像)

图2 实时跟踪数据库交易日志

图3 给出了第三种解决方法。该方法使用数据库表作为临时消息队列。服务在更新了聚合后,就将事件作为本地ACID 事务的组成部分插入到名为EVENTS 数据库表中。EVENTS 表被一个独立的进程轮询,该进程将事件发布到消息代理。该方法的优点之一是服务可以发布高层业务事件,而不好的一面是该方法容易出错,因为事件发布代码必须与业务逻辑同步。

(点击放大图像)

图 3 使用数据库表作为消息队列

以上三种方法都有显著的缺点。发布到消息代理再做更新的方法并未提供读自写(read-your-writes)一致性。实时跟踪事务日志的方法提供了一致读,但是发布的业务事件不会总是高层业务事件。使用数据库表作为消息队列的方法提供了一致读并可发布高层业务事件,但是该方法依赖开发人员实现状态变更即发布事件的机制。幸好还有另外一种解决方法,即以事件为中心实现持久化和业务逻辑的方法,称为事件溯源。

使用事件溯源开发微服务

事件溯源是一种以事件为中心实现持久化的方法。事件溯源并非一个新理念。我在五年多前就听说过事件溯源,但是直至我着手去开发微服务时,它依然是一个新奇事物。下面你们将会看到,事件溯源是一种实现事件驱动微服务架构的好办法。

使用事件溯源的服务将每次聚合持久化为一系列的事件。在创建或更新一个聚合时,服务将一个或多个事件保存在数据库中,这时我们也将数据库称为事件存储。事件溯源通过加载并回放事件,重建了聚合的当前状态。在函数式编程理念中,服务通过对事件执行 reduce(在函数式编程中多称为 fold)操作重建聚合的状态。因为事件就是状态,自动更新状态和发布事件的问题不再存在。

回到上文的例子,Order 服务并非将每个订单都作为 ORDERS 表的一行数据,而是持久化每个 Order 聚合为一系列的事件,保留“Order Created”、“Order Approved”、“Order Shipped”等事件。图 4 显示了这些事件在基于 SQL 的事件存储中的存储情况。

图 4 使用事件溯源持久化 Order 服务。

表中各列的意义如下:

  • entity_type 和 entity_id 列:用于标识聚合。
  • event_id:标识事件。
  • event_type:事件类型。
  • event_data:序列化成 JSON 的事件属性。

一些事件中包含了大量的数据。例如“Order Created”事件就包括了全部订单,其内容涉及订单的单项产品、支付信息和快递信息等。而 Order Shipped 等事件包含了少量数据甚至不包含数据,这些事件只是表示状态的转移。

事件溯源和事件发布

严格意义上讲,事件溯源仅是将聚合持久化为事件,但是将事件溯源作为可靠的事件发布机制使用也是非常简单的。保存事件在本质上是一个原子的操作,它确保了事件存储将向感兴趣的服务交付事件。举个例子,如果事件存储于上文所介绍的 EVENTS 表中,为了处理新的事件,订阅者可以直接去轮询 EVENTS 表。更为复杂的事件存储将使用到另一种方法,该方法具有相似的一致性保证,但是具有更高的性能和扩展性。例如, Eventuate Local 使用事务日志进行实时跟踪,从 MySQL 的复制流中读取插入到 EVENTS 表中的事件,并将这些事件发布到 Apache Kafka。

使用快照改进性能

Order 聚合具有相对较少的状态转移,因此它只有少量的事件。在事件存储中查询这些事件并重建 Order 聚合是很高效的。不过有些聚合会包含大量的事件。例如 Customer 聚合可能包含大量的 Credit Reserrved 事件。久而久之,在这些事件上的 load 和 fold 操作将会日渐低效。

一个常用的解决方法是将聚合状态周期性地持久化为快照。通过加载最近的快照以及仅在快照创建后发生的事件,应用可以恢复聚合的状态。从函数式编程的角度看,快照是 fold 操作的初始值。如果聚合具有一个易于序列化的基本结构,那么就可以使用它的序列化格式作为快照,比如 JSON。更为复杂的聚合可以通过使用备忘录模式(Memento pattern)形成快照。

在网店例子中,Customer 聚合具有一个非常简单的结构,其中包括客户信息、客户的信用额度以及客户的信用余额。Customer 聚合的快照仅是该聚合状态的JSON 格式。图5 显示了如何从Customer 快照中重新创建一个Customer,这个快照所对应的是Customer 在发生#103 事件时的状态。Customer 服务只需加载该快照和事件#103 之后发生的事件。

图5 使用快照优化性能。

通过反序列化快照的JSON,Customer 服务可以重建Customer 聚合,然后可加载并应用从#104 到#106 的所有事件。

实现事件溯源

事件存储是数据库和消息代理的混合体。将事件存储看作是一个数据库,因为它具有通过主键插入和检索聚合事件的API。另一方面,事件存储也是一个消息代理,因为它具有订阅事件的API。

事件存储有一些不同的实现方法。一种方法是编写你自己的事件溯源框架。例如可以在RDBMS 中持久化事件。订阅者通过轮询EVENTS 表找到事件,这种事件发布方法简单,但是低效。

另一种方法是使用专用的事件存储,这些专用的事件存储通常会提供丰富的特性、更好的性能和可扩展性。事件溯源的先行者Greg Young 就提供了一个基于.NET 的开源事件存储,称为“ Event Store ”。他所创立的公司以前称为 Typesafe,提供一种称为 Lagom 的基于事件溯源的微服务框架。我的初创公司 Eventuate 也以云服务形式提供了一种用于微服务的事件溯源框架,该架构是基于 Kafka/RDBMS 的开源项目。

事件溯源的优缺点

事件溯源具有其优缺点。一个主要优点是无论聚合的状态如何改变,事件溯源都能可靠地发布事件。这为事件驱动微服务框架提供了很好的基础。此外,因为每个事件都可以记录做用户的标识信息,所以事件溯源提供了准确的审计日志。事件流可用于很多其他的目的,包括向用户发送通知以及应用集成。

另一个优点是事件溯源存储了每个聚合的完整历史。你可轻易地实现基于时间的查询,去检索聚合的历史状态。为确认特定时间点上的聚合状态,你只需对到该时间点为止所发生的事件做 fold 操作。例如,可直接实现计算过去某个时间点上客户的可用信用额度。

事件溯源也在很大程度上避免了 O/R 阻抗匹配问题。这是由于它持久化了事件而非聚合。事件通常具有简单的、易于序列化的结构。通过将聚合状态序列化为备忘录,服务可以对复杂聚合进行快照。备忘录模式在聚合及其序列化之间添加了一个中间层。

当然,事件溯源也并非“银弹”,它也存在一些缺点。事件溯源是一种不为人所熟悉的编程模型,因此存在一定的学习曲线。对于已有应用,为使用事件溯源,你必须重写已有应用的业务逻辑。好在这是一个相当机械化的转换过程,可以在将应用迁移到微服务时完成。

事件溯源的另一个缺点是消息代理通常保证至少一次交付。非幂等的事件处理器必须检测并丢弃重复的事件。通过赋予每个事件一个单调递增的标识符,可使事件溯源框架发挥作用。然后事件处理器可以通过追踪最大事件的标识符去检测重复的事件。

事件溯源的另一个挑战是事件的模式(以及快照!)会随时间演化。因为事件是永久存储的,在重建聚合时,服务可能需要对多个模式版本对应的事件做 fold 操作。一种简单的方法是让事件溯源框架在从事件存储中加载所有事件时,将所有的事件转换为最新版本的模式。最后,服务只需要对最新版本的事件做 fold 操作。

事件溯源的另一个缺点在于对事件存储的查询将面临挑战。例如,假设你需要找到信用额度较低但是信用良好的客户,仅是编写查询语句 “SELECT * FROM CUSTOMER WHERE CREDIT_LIMIT < ? AND c.CREATION_DATE > ?”并不能解决问题,因为并不存在包含信用额度的列。你必须使用更为复杂而低效的包含嵌套 SELECT 语句的查询来计算信用额度,通过对信用的初始值事件和调整事件做 fold 操作计算出来。此外,通常基于 NoSQL 的事件存储只支持基于主键的查找,这让事情变得更加糟糕。基于上述原因,实现必须要使用称为命令查询职责分离(CQRS,Command Query Responsibility Segregation)的方法。

使用 CQRS 实现查询

事件溯源是在微服务架构中实现高效查询的一个主要障碍,但并非是唯一的问题。例如,查找下过大额订单的新客户的 SQL 查询:

复制代码
SELECT *
FROM CUSTOMER c, ORDER o
WHERE
c.id = o.ID
AND o.ORDER_TOTAL > 100000
AND o.STATE = 'SHIPPED'
AND c.CREATION_DATE > ?

在微服务架构中,你不能在 CUSTOMER 和 ORDER 表间做连接操作。因为每个表属于不同的服务,只能通过服务的 API 来访问。你不能对属于不同服务的表编写传统查询实现连接查询。事件溯源会让事情变得更加糟糕,它会阻止你编写简单直接的查询。让我们看一下在微服务架构中实现查询的方法。

使用 CQRS

一种好的查询实现方法是使用称为“命令查询职责分离(CQRS)”的架构模式。正如其名字所示,CQRS 将应用分割为两个部分。第一部分是命令端的,用于命令处理(例如,HTTP POST、PUT 和DELETE),实现创建、更新和删除聚合。当然这些聚合是使用事件溯源实现的。应用的第二部分是查询端的,通过查询聚合的一个或更多的物化视图实现查询处理(例如HTTP GET)。通过订阅命令端服务的事件,查询端将保持视图与聚合的同步。

每个查询端视图可使用任何类型的数据库实现,只要该数据库对要求的查询提供良好的支持。根据需求不同,应用的查询端可使用一到多个下表所列的数据库:

表1 查询端视图存储

如果你需要……

可以使用……

例如……

基于主键查找JSON 对象

文档数据库,例如 MongoDB ,或者键值数据库,例如 Redis

通过维护包含客户订单的客户 MongoDB 文档实现订单历史。

基于查询查找 JSON 对象

文档数据库,例如 MongoDB。

使用 MongoDB 实现客户视图。

文本查询

文本搜索引擎,例如 Elasticsearch

通过维护每个订单的 Elasticsearch 文档,实现对订单的文本搜索。

图查询

图数据库,例如 Neo4j

通过维护客户、订单和其他数据的图结构实现欺诈检测。

传统的 SQL 报表 /BI

RDBMS

标准业务报表和分析报告。

使用 RDBMS 作为存储记录的系统,并使用全文搜索引擎来处理查询,比如 Elasticsearch,这种方式应用很广泛。从很多方面来看,CQRS 是这种方式的一般化实现,而且是基于事件的。CQRS 不仅限于文本搜索引擎,还可使用非常宽泛类型的数据库。此外,CQRS 可以通过事件订阅近乎实时地更新查询端视图。

图 6 显示了在网店例子中对 CQRS 模式的应用。Customer 服务和 Order 服务是命令端服务,提供了创建和更新 Customer 和 Order 的 API。Customer View 服务是查询端服务,它们提供查询 Customer 的 API。

图 6 在网店例子中使用 CQRS。

Customer View 服务订阅了由命令端服务发布的 Customer 和 Order 事件。该服务更新由 MongoDB 实现的视图存储。服务维护了 MongoDB 所存储一系列文档,每个用户一个文档。每个文档中包括了客户细节信息的属性,还包括存储客户近期订单的属性。这一系列文档支持多种查询,包括之前所提到的查询。

CRQS 的优缺点

CQRS 有其优点,也有缺点。一个主要优点是 CRQS 使得在微服务架构中实现查询成为可能,尤其是使用事件溯源的微服务架构。CRQS 让应用可以高效地支持多种类型的查询。另一个优点是关注点的分离会简化应用的命令查询端。

CQRS 也存在一些缺点。其中的一个缺点是 CRQS 需要对系统的开发和操作做额外工作。你必须开发并部署那些更新和查询视图的查询端服务。此外,你还需要部署视图的存储。

另一个缺点在于 CRQS 需要处理命令端和查询端之间的“延迟”。正如你能想到的,命令端的更新在查询端生效会有一定的延迟。如果客户应用更新聚合后就立刻查询视图,该应用将看到前一个版本的聚合。在编写客户应用时,必须避免把这种不一致性暴露给用户。

总结

使用事件维护服务间数据一致性的主要挑战在于更新数据库和发布事件的原子性。传统的解决方案是使用跨数据库和消息代理的分布式事务。但是对于现代应用,两阶段提交并非可用的技术。更好的方法是使用事件溯源,事件溯源是一种以事件为中心的方法,用于逻辑设计和持久化。

查询是使用微服务架构中要面临的另一个挑战。通常查询需要连接属于多个服务的数据。但是由于数据对于服务是私有的,连接运算不可直接实现。由于当前的状态并非显式地存储,使用事件溯源也增加了查询的难度。解决方案是使用命令查询职责分离(CQRS,Command Query Responsibility Segregation),维护一个或多个聚合的物化视图。

Chris Richardson是一位程序开发人员和架构师。他还是一名 Java 冠军程序员(Java Champion),同时也是《用轻量级框架开发企业应用》一书的作者。这本书的内容是关于如何使用 Spring 和 Hibernate 等框架去构建企业级 Java 应用。Chris 也是 CloudFoundry.com 的创始人。他还担任了一些企业的顾问,指导企业如何去改进应用的开发和部署过程。他目前正致力于发展他的第三个初创企业。读者可以通过 Twitter 账号 @crichardson Eventuate 网站联系到 Chris。

查看英文原文: Developing Transactional Microservices Using Aggregates, Event Sourcing and CQRS - Part 2


感谢薛命灯对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-02-12 16:3812755
用户头像

发布了 227 篇内容, 共 74.0 次阅读, 收获喜欢 28 次。

关注

评论

发布
暂无评论
发现更多内容

缓存 | Redis 缓存避坑指南

RadonDB

数据库 redis

【架构实战营】模块五作业

Abner S.

#架构实战营

☕【Java技术指南】「开发实战专题」Lombok插件开发实践必知必会操作!

洛神灬殇

Java 编译 lombok 8月日更

基于java springboot vue uniapp商城源码(毕设)

清风

Java uniapp 商城项目 毕业设计

华为海外女科学家为您揭秘:GaussDB(for MySQL)云栈垂直集成的力量有多大?

华为云开发者联盟

数据库 云数据库 GaussDB(for MySQL) 云栈 事务数据库服务

科技平台与社会的和谐相处

CECBC

数字人民币银银合作以及平台接入的模式分析

CECBC

赋能智慧社区,多维度提升管理质效

CECBC

Android SDK 启动退出方案演进

神策技术社区

大前端 后端 代码 数据采集

百度世界2021:百度大脑升级、昆仑芯2量产、智能云加速AI落地爆发

百度大脑

人工智能 百度大脑

为什么代码会有好坏?

鉴释

程序员 代码 代码规范

菜谱系统小成阶段,Python Web 领域终于攻占一个小山头

梦想橡皮擦

8月日更

一文了解全球主要经济体对区块链技术的采纳情况和监管政策

CECBC

【墨天轮专访第一期】人大金仓:国产数据库的竞争本质就是人才的竞争

墨天轮

数据库 国产数据库 KingBase 人大金仓

抖音快手短视频询盘系统开发

抖音快手短视频SEO系统开发

抖音快手短视频平台获客系统开发内容

接口文档生成工具 一键生成文档 ApiPost

CodeNongXiaoW

项目管理 大前端 测试 后端 接口管理工具

接口管理工具APIPOST的预/后执行脚本里,常见的响应参数变量和常用方法集合——apipost

Proud lion

大前端 后端 Postman 开发工具 接口文档

MySQL 系列教程之(八)DQL:子查询与表连接

若尘

MySQL 数据库 8月日更

如何做上线前的实操演练?

boshi

项目管理

Go语言那些事儿之浅谈协程并发竞争资源问题

Regan Yue

Go 语言 8月日更

Go 语言, 一文彻底搞懂 map 实现原理

微客鸟窝

Go 语言 8月日更

《MySQL系列》 InnoDB行记录存储结构

Silently9527

MySQL 面试 innodb innodb行记录

抖音快手短视频SEO营销系统软件开发价格

带你读AI论文丨用于目标检测的高斯检测框与ProbIoU

华为云开发者联盟

算法 数据集 目标检测 高斯检测框 ProbIoU

【从零开始学爬虫】采集当当网图书商品信息

前嗅大数据

大数据 爬虫 数据采集

web技术分享| 实现WebRTC多个对等连接

anyRTC开发者

音视频 WebRTC JavaScrip web技术分享

抖音快手短视频营销软件系统开发案例

短视频go研发框架实践

百度Geek说

百度 架构 后端 短视频 hulk

React Native 页面浏览事件采集方案 | 数据采集

神策技术社区

大前端 后端 代码 数据采集

使用聚合、事件溯源和CQRS开发事务型微服务(第二部分)_语言 & 开发_Chris Richardson_InfoQ精选文章