本文是将介绍如何在微服务中实施CQRS模式,并深入探讨为什么无服务器和这种类型的系统无比契合。在最后,将介绍一个使用Spring Cloud Stream实施CQRS的参考应用。
什么是事件驱动型架构?
事件驱动型架构会优先处理领域事件,但这种架构已经逐渐被淘汰。
我们日常使用的一个示例是前端应用。在如今使用的Web浏览器中,事件的处理方法是捕获用户表单的输入数据,而连接到页面元素的事件将由一个显式映射函数来处理,在触发时在用户界面应用对状态的更改。
最近,随着微服务的广泛普及,人们对如何在分布式后端系统中利用事件驱动技术重新产生了兴趣。
CQRS
如今,事件驱动型架构中最热门的实践之一叫做CQRS,全称是命令查询责任分离。CQRS是一种架构风格,能让您使用不同模型来更新和读取领域数据。
CQRS的基本理念是,您用来更新和读取数据的模型要相互分离是很自然的事情。上图描述了这种基本思想。
CQRS之所以在事件驱动型架构中如此受欢迎,是因为作为输入的领域事件与其所属的领域模型在结构上有所差异。以下面这个代表一个帐户的领域模型对象为例。
示例1.帐户整合
- {
- "createdAt":1481351048967,
- "lastModified":1481351049385,
- "userId":1,
- "accountNumber":"123456",
- "defaultAccount": true,
- "status":"ACCOUNT_ACTIVE"
- }
当某个服务想查询帐户时,就会使用该模型。那么,如果我们要将状态更新为ACCOUNT_SUSPENDED,该怎么办?通常,只需简单更新领域对象的状态字段即可。现在,如果我们想使用领域事件来更新状态,该怎么办?由于领域对象在结构上与事件不同,我们需要一个可将不同模型作为命令接受的API。
以下代码段是将帐户状态从ACCOUNT_ACTIVE转换为ACCOUNT_SUSPENDED的领域事件。
示例2.帐户事件
- {
- "createdAt":1481353397395,
- "lastModified":1481353397395,
- "type":"ACCOUNT_SUSPENDED",
- "accountNumber":"123456"
- }
要处理此领域事件并将更新应用到查询模型,我们必须有接受命令的API。该命令将包含领域事件的模型,并使用模型来处理对帐户查询模型的更新。
将命令模型与查询模型相分离是对CQRS最简单的解释。我们如今常见的复杂性更多地与实施类型有关,将模式应用到微服务时尤为如此。
CQRS和微服务
如果CQRS与微服务相结合,毫不夸张地说,情况就变得有点复杂了。我们来看看使用Spring Boot实施CQRS的“简单”微服务是什么样。
上图是CQRS模式实施的简略图示。图中,我们将一个微服务分为命令端、查询端和事件处理器,这三个部分可以相互独立地部署。
命令端
本示例中的命令端提供了REST API,可接受通过HTTP发送的请求。请求采取命令的形式,可以驱动对微服务所拥有的领域数据的状态更改。简单来说,对领域数据的任何写入都将以命令形式从API请求流出,处理导致数据库发生更改的操作。
命令触发操作,操作触发领域事件。领域事件保存在事件存储中,也就是“一个将数据库与消息代理相结合的系统。”最合适入门使用的事件存储是Eventuate,是由Chris Richardson创建的项目,旨在帮助将CQRS和Event Sourcing应用到微服务。
领域事件存储为按时间顺序排列的一系列事件,附加在日志上。由于每个命令都生成一个事件,我们能够根据收集的事件历史记录重新构建当前系统的总体状态。
事件处理器
我们要探讨的下一个组件是事件处理器。这个CQRS组件采用Worker应用的形式,负责摄取领域事件。事件处理器是无状态的,并侦听来自事件存储的消息,对传入的事件消息采取操作。
事件处理器可通过很多有用的方式对新的领域事件作出响应。一个领域事件可以生成多个事件,这些事件可以发送到其他微服务。这就是为什么大多数微服务开发人员都被CQRS吸引,因为通过这种方法可以发布和订阅来自限界环境之外的应用的领域事件。
这种方法为我们提供了一种机制,可确保领域数据的引用完整性。来自其他微服务的消息可用来处理领域事件,从而让我们能够维护分布式系统中与其他记录的领域数据相关的恼人的外键关系。
查询端
事件处理器首要负责应用可改变领域整合状态的领域事件。每个领域事件都可用来更新数据库记录,形成描述整合的增量实体化视图。反过来,查询端将提供一个REST API,允许HTTP客户端读取从已处理事件生成的实体化视图。
查询端组件中的限制是领域数据是只读的。此系统中的所有状态更改都会从命令端流入,然后形成可在查询端读取的实体化视图。
是分布式一体化还是微服务?
如今,大多数人想到单个微服务时,他们想到的是一个独立的服务组件。在大多数情况下,微服务被构建为应用,专注于处理好一件事情。最重要的是,这种服务可以独立于其他服务进行升级和部署。
现在,如果说到传统的CQRS实施,如果由于组件相互分离就称之为微服务,似乎有些不妥。因此我们得问一句:可以将CQRS应用视为微服务吗?或者换种说法,可以像某些开发人员那样,开始称之为分布式一体化吗?对于这个问题,不同的人会有不同的答案。微服务主要是让小型独立团队有能力持续交付功能,作为其他微服务组成的更大生态系统的一部分。
如果与大多数微服务部署相比,CQRS部署是十分复杂的。对于微服务团队来说,能够持续将功能交付至生产是目标。由于CQRS中分离的组件仍可以独立部署,我们可以说每个部署单元仍可满足独立将功能交付至生产的最低要求。微服务的一个功能应该始终需要且最多一个可部署单元。当交付一项功能时需要同时协调部署多个单独组件,就会产生所谓的分布式一体化。
微服务和无服务器
无服务器也称为FaaS(功能即服务),可让您在不需要设置或管理应用服务器或容器的情况下,将代码部署为功能。无服务器是一种新型架构风格,在构建和运行云原生应用方面越来越受到关注。使用无服务器功能的一个显著优势是,事件概念被优先处理。
有很多人以为,微服务和无服务器不兼容,彼此的架构风格完全不同。但是回想一下前面提到的CQRS你就会明白,这种想法是错误的。让我们来看一个场景,你认为在此场景中微服务的边界在哪里?
一种方法是认为微服务的边界就是团队的边界。只要一个团队可以独立、连续地将特性部署为功能,那么微服务的边界就只是负责为团队拥有的特性提供支持的功能子集。
权衡
要采用微服务和无服务器相结合的方法,需要您仔细权衡多项内容,让我们来看看需要注意的事项。
速度
对于微服务来说,速度就是目标。我们可以通过关注以下两个问题来衡量速度,平均时间越低,团队交付功能的速度就越快。:
开发人员改变一行代码并将其安全地部署到生产中的速度有多快?
新开发人员快速上手并安全地更改代码库的速度有多快?
无服务器具有学习曲线,但有助于提高微服务的速度。它将大量工作流程管理从核心组件移出,放入独立升级和部署的小型可组合功能中,从而提高速度。这在大程度上缩短了开发人员了解单个功能的工作原理以及如何安全更改所需的时间。
无服务器功能也易于升级或部署,但可能会让了解整体系统更加复杂。将数百个无服务器功能作为一个整体来管理,这听起来就有点头大。
复杂性
软件中的复杂性无可避免,随着代码库逐渐老化,复杂性也随时间增加。当复杂性增加,或者当框架或语言过时的时候,一体化应用就变得笨重、难以改变。微服务器将这种复杂性分解到分布式系统中,其中的每个可部署单元都易于理解,易于由一小部分敏捷开发人员进行更改。
云原生CQRS参考应用
这里有一个将云原生CQRS应用,构建为事件驱动型微服务和无服务器功能相结合的参考应用。