核心要点
- CQRS 架构将写入新信息的操作放到一个单独的应用中,将它与读取信息的操作进行分离;
- N 层架构和 CQRS 模式分别适用于不同的特定实现;
- CQRS 模式适用于复杂、具有多步操作的系统,或者具有多种请求类型的系统中;
- CQRS 能够提升技术和业务团队的协作,在相互交流和编码方面,会采用一种通用的语言;
- 如果使用恰当,CQRS 有助于管理系统的复杂性。
CQRS 这个术语指的是一种软件架构模式和概念,从事领域驱动开发(Domain Driven Design)的软件工程专家最终都会碰到这个模式。但是,它背后的理念是什么呢?在有些场景下,CQRS 为什么是比常见的 N 层软件架构更好的可选方案?这两种模式对比起来,各有什么特点?另外,更重要也真正的值得关注的是,“它到底能为业务带来什么?”我们试图在更为宏观的层面来剖析 CQRS 应用架构能够带来什么价值。
聚焦 CQRS:底线是什么?
那么,基于 CQRS 的应用架构的关键点是什么呢?我们先从概念的定义开始吧。
命令查询职责分离(CQRS,Command Query Responsibility Segregation)是一种应用架构模式,它会将应用分为两部分:查询部分(查看模型)和命令部分(写入模型)。每一部分都负责处理特定的操作集——分别也就是读取类型和写入类型。CQRS 概念最初是由 Greg Young 提出和积极倡导的。它是 CQS(命令 - 查询分离)理念的自然延伸,CQS 理念由 Bertrand Meyers 提出,主张将方法分为命令和查询。CQRS 使用了相同的原则,不过将其扩大到了整个系统中。
按照这种架构,业务逻辑层的两个组件将会相互独立地运行。因此,读取模型(Read Model)将会处理用户的查询——在处理能力方面的要求会更少一些,而写入模型(Write Model)将会经历一个很长的处理路径,包括校验、队列、消息以及用户命令要执行的业务规则处理。
CQRS 模式受到模型驱动开发(Domain Driven Design)拥护者的推崇。这种方式强调在实现应用的时候,将解决业务问题放在第一位。它关注的中心点在于业务领域及其发挥作用的上下文之间的紧密协作。它会聚焦于业务而不是技术问题,并且要找出特定领域中所有细微的内容,这之所以能够实现要归功于采用了一种通用语言(Ubiquitous language)——能够被实现团队、业务分析师、领域专家以及其他人员理解的语言。这门语言能够让所有的团队成员(业务和技术)共享工作成果,他们会定义通用的业务对象来描述解决方案的领域模型,还会定义这种模型中的特定上下文,在这一点上要取得共识。随后,这些业务对象会被代码本身来使用。
因为基于 CQRS 的架构允许将逻辑分为两种不同类型的任务,所以它创造了一个有利条件,能够使用这些明确的对象来编写代码并且能够强化以业务为中心的 DDD 方式的影响力。技术团队更加关注于业务规则和处理流程,而不是基础设施或者不同类型的架构所带来的代码冲突。鉴于这一点,CQRS 和领域驱动设计通常会一拍即合。
CQRS 与 N 层架构相比有何优势?
…或者说我们什么时候该去仔细审视 CQRS 呢?
在 Web 中,最常用的一种架构就是典型的多层架构。在它的一个简化版本中,应用会分为三层:用户界面、业务逻辑和数据库层。在这种架构中,查询(读取系统状态的请求 / 要展现某些数据的请求)和命令(改变系统状态的请求 / 写入新数据或对数据进行变更)会在相同的业务逻辑层来进行处理。
何时着手行动为好呢?
如果有人想要按照路径最短且最安全的方式从 A 到达 B 的话,那么他应该不会尝试既漫长又乏味的路线。类似的,如果一个 Web 解决方案就是相对静态的品牌展示功能,几乎没有用户的交互性输入(假设只有类似于简单的联系人表单这样的东西),那么我们肯定不希望采用过于复杂和成本高昂的系统。但是,随着更多的业务规则和交互点添加到应用逻辑之中,解决方案就不会那么简单了。
举例来说,我们仔细看一下典型的在线商店解决方案是如何运行的。
当用户选择了一件喜欢的商品并准备购买的时候会发生什么呢(换句话说,在电子商务系统中,当我们想要写入购买命令的时候会发生什么)?当用户点击了购买按钮,首先,需要检查所请求条目的可用性。如果系统的响应确认所需的数量能够满足,那么就会邀请用户填写订单表单并提供派送地址、账单信息、支付信息以及其他完成购买行为所需的详细信息。然后,用户点击支付按钮,系统将会运行所有的校验过程(派送地址校验、销售税校验、账单地址校验、信用卡信息校验以及余额校验等)。当这些过程成功完成之后,用户会看到一个确认信息,包含了订单的细节和后续操作的指南,并且会对系统状态进行相应的变更(更新仓库产品的可用状态,注册新的购买订单并将投递细节发送给仓储团队等)。
现在,我们看一下如果不采取这种方式的话,用户还可以怎样与在线商店进行交互。我们的用户可以向电子商务系统发送什么样的读取请求呢?在线商店的访问者在尝试使用系统的时候,可以点击某个商品条目来查看其特征的详细描述,仔细查看可能会购买的商品。在这种情况下,访问者将会发送对单个商品页面的读取请求,这个页面将会展现给访问者。用户还可以搜索我们的商店,查找感兴趣购买的商品或者得到它的更多信息。例如,如果在线商店是专门销售移动电话的,那么有些用户在访问我们的电子商务方案时,可能会根据特定移动设备的型号进行搜索。对于这种请求,我们的系统应该为 UI 返回一个搜索结果页面,结果中包含了匹配用户搜索的信息。最后,有些用户可能还会根据特定的参数如设备制造商,来查找分类后的商品条目,或者是使用匹配其断言(如屏幕分辨率、内存容量、大小、外观和体验等)的过滤器来对商店进行搜索。在上述的所有情况中,我们都需要处理传入的查询、搜索我们解决方案的数据库并生成相关的 UI 视图,在这个生成的视图中需要包含用户所请求的信息。
在 N 层结构的解决方案中,购买商品的行为会在逻辑层遵循相同的路径,这与用户查询各种条目并展现在 UI 上相同。但是,购买命令将会进行商品的可用性检查和各种类型的校验,而在线商店的查询命令会触发的过程包括元数据搜索、过滤数据存储中的信息并组合相关的 UI 视图。
我们可以看到,在通用的 N 层架构中,不同类型的操作会不均衡地经过相同的应用层。
你可能会问“这有什么问题吗?”
如果某家公司非常满足于它在行业中的现状,那么这种类型的架构对它非常合适。如果应用的用户不会随着时间推移而变化,如果要处理的信息量始终维持在相同的水准,如果应用不会经历高负荷的时间段或非典型的流量峰值,如果应用始终在它预先定义好的容量范围内运行,那么多层应用的架构几乎不会遇到任何的问题。但是,在现实生活中,我们遇到这种场景的频率又有多高呢?
在现实生活中,在应用发布之后的一段时间内,如果应用有发展推动力的话,那么它的性能就会经历一定的考验。随着用户数量的增长,要维持相同等级的可用性和故障恢复标准,就需要更多的系统资源。
在这种情况下,业务管理者就会开始考虑提升已有系统的性能。
但是,如果像 N 层应用这样,查询和命令使用同一个业务逻辑层的话,那么系统从一开始就会承受不必要的压力,当请求的数量不平衡,更多数量的请求都是某一种类型时(通常来讲,更多的都是简单的读取),这一点尤为明显。在这种情况下,可用资源的使用效率低下,因为系统会选择一个很长的路径,而不是更合理的捷径。
其次,对于系统的未来发展而言,N 层架构代表了一种更复杂的方式。如果系统的负载因为某种类型的操作增加(假设查询的数量出现了增长),出现不均衡的增长时,我们并没有办法按照期望的方式扩展系统的规模。在多层方案中,开发团队如果想针对特定类型操作的业务逻辑进行细粒度调优的话,这是很难实现的。我们必须提升整个应用层的处理能力,才能确保性能能够达到用户的期望。采用这种方式来扩展系统的话,将会导致一些操作在系统中会执行很长的路径,而不是从 A 到 B 的捷径。这同样会导致系统资源的低效分配。
上面提到这些方面恰好是 CQRS 优于传统 N 层架构的地方。
是什么让 CQRS 成为一种有价值的架构模式呢?
我们放弃熟悉和可靠的架构,转而选择一种看似混乱且复杂的架构,对此所有的质疑都是情有可原的。但是,假设我们要着手创建一个高级的 Web 解决方案,这个方案有着复杂的协作上下文环境,在写入模型中有多种命令类型,同时还要为多并发用户提供高质量的在线服务,那么我们仔细权衡一下 N 层应用架构的上述瓶颈,也就是性能、可扩展性以及效率,然后考虑一下基于 CQRS 的架构所能带来的收益:
- **业务驱动开发与通信,自动化的代码测试** 因为我们会将应用中的命令和查询部分进行分离,代码的语法会变得更加简单。应用中两种不同功能所形成大杂烩会少很多。代码能够清晰地反映在特定的业务域中操作的真实意图。命令和查询的实现表达了对应的业务流程。现在,开发团队能够从那些针对客户的技术官话中解脱出来。转而使用更为通用的技术语言,因为技术和业务团队都会同意使用一种语言,也就是通用语言(Ubiquitous language)来描述解决方案的业务领域。代码本身对于非技术人员会更加清晰,参与项目的每个人都能很好地互相理解。现在,因为非技术的团队成员也能阅读代码,所以他们也能负责编写测试用例模式,这些模式稍后可以用到解析源码和自动化测试用例生成的过程中。
- **特定组件的优化和扩展,更高效的资源使用率** 借助基于 CQRS 的架构,除了数据库层级的通信之外,应用的两个组件之间不会互相交叉。这样的话,按照前文所述,我们能够将其中的每一项都优化至最佳的性能,完成它们所设计的一组任务。读取模块可以进行增强,确保最短的反应时间,为用户交付所请求的 UI 信息,其中会包含读取数据库所形成的非规范化视图。另外,写入模型可以强化安全性并突出业务领域本身的开发。借助这种隔离的原则,解决方案可以根据业务环境需求的变化或不断进步的技术标准进行独立地扩展。可扩展性的问题需要优先处理,最紧急的事情能够快速得到解决,同时还不会浪费系统资源,也不会像有些人想的那样试图去提升整个应用程序的业务逻辑处理能力。
- **高负载场景下的可持续性与性能** 如果应用具有很复杂的业务逻辑,并且应用只有其中的某一部分会有很高的负载,那么 CQRS 模式会是一个很好的技术解决方案。它能够避免无效任务运行所带来的负担,或者减少复杂算法领域的“背景代码噪音(background code noise)”,因为这种噪音会消耗系统的性能。例如,用户对于数据输入的操作在响应时间上会有更高的容忍度,但是对于读取请求却并非如此。查询响应每增加一毫秒都可能会导致成千上万美元的损失。考虑到延迟阈值的 3 秒钟基准,读取模式中的应用响应性和可用性就显得尤为重要了。在关键的地方,CQRS 能够让实现最优的性能成为可能。
- **灵活的人员配置,员工流失所带来的风险更小** 将软件解决方案中的读取和写入模型进行隔离,并让它们尽可能实现自治,这样的话就意味着我们能够对它们很轻松地进行单独处理,并行地开展工作,并不需要担心通用的约定和这些模型之间的协调。读取模型的开发任务可以交给较为初级的专业人员,因为这样的代码很少会出现严重的错误。如果自身的开发能力无法涵盖所有内容的话,这部分甚至可以外包给外部的团队。我们不会陷入内部专家之间的沟通交流问题之中,编写应用写入模块的团队和负责读取模块的团队之间不会因沟通而出现麻烦。另外,代码本身会变得非常简单直接,它会成为一个已编写好的知识库。新的团队成员通过阅读代码能够快速掌握业务规则,这意味着将来支持已有的解决方案可以由一个全新的团队很容易地完成。
- · **技术栈的选择更加具有开放性**CQRS 为我们提供了一种可能性,让我们能够避免严格的技术栈选择这一乏味的过程。当应用服务的目标确定后,在语义上不同的区域是相互独立的,这两块区域并不一定必须保持一致,因此可以独立地为这两块区域选择技术。没有必要再去理清这些额外的依赖,也没有必要为读取和写入模型选择统一的“一刀切”式的技术栈。假设我们需要实现一个事务性系统,这个系统需要有内置的报表工具。在实现这种解决方案的时候,开发团队可以为事务处理部分的写入模型选择一组技术栈,为读取模型的报表功能选择另外一组技术栈。在这种情况下,前者适用于将事务写入到系统中,而后者的数据结构和查询机制可以进行优化,从而更好地适应搜索系统并快捷地生成所需的报告界面。
- **整个方案的总拥有成本会更低** 由于这种模式天然的灵活性,并且会将应用层的处理操作进行隔离,因此 CQRS 解决方案在长期来看会更划算。在启动阶段,基于 CQRS 的架构所能带来的好处可能并不那么明显,因为在应用架构上它需要更详细地规划,同时还会涉及到更多的实际编码工作。但是,就像其他已有的开发实践一样,它们在演进的过程中都会聚集一些经验丰富的社区支持者,CQRS 也有很多的倡导者和支持者。我们可以找到大量可重用的资产和制件,基于它们进行构建就能减少技术团队很多的麻烦。在网上,我们可以看到大量有用的 CQRS 框架和文档。如果在 CQRS 方面进行了投资的话,公司将来能够获得更大的灵活性,并且在随后的维护、支持以及扩展性方面也能收益。
在什么情况下,我们应该对 CQRS 实现说“不”呢?
它适用于所有的地方吗?CQRS 是针对所有软件开发任务的银弹吗?当然不是!对于一些技术驱动的业务解决方案来说,这种模式特别合适,但是在其他的一些场景下,则是完全不推荐使用它的。那么,在什么场景下不适合使用 CQRS 呢?如果整个软件系统,你想从头到尾都想采取 CQRS 的话,那这就是不合适的。
如果我们在设计复杂应用程序的时候,缺乏模块化的结构,并试图将 CQRS 模式应用到整个系统之中,那么很有可能会形成一堆像迷宫一样的代码,整个代码结构都会非常怪异。首先,解决方案架构要拆分为更小的单元(边界上下文),只有在此基础上,其中的一部分或全部单元才能使用 CQRS 进行优化。
边界上下文(Bounded context)最初是领域驱动设计所使用的术语。简单来讲,我们会在特定的业务领域中创建边界上下文,它们对应了领域中逻辑一致的各个组成部分。在软件系统中,它们就是具有特定职责的更小的组件,通常会满足特定部门的需求(或者是部门中具有明确职责范围限制的业务单元)。每个上下文的职责不会重叠。例如,如果你有一个整个组织都会使用的工作流管理系统,它可能会包含 IT 部门的上下文、会计部门的上下文、销售部门的上下文、客户支持部门的上下文等等。这些上下文会有着清晰的边界,但是会共享一些理念,比如客户销售和会计上下文。因此,在复杂的系统中,如果我们能够将 CQRS 应用到特定模块上,而这些模块分别对应于业务领域中的上述边界上下文,那么这样的决策就是非常明智的。
小结
在前文中,我们列出了 CQRS 模式的收益和局限性,对基于 CQRS 的实现进行适当的考虑和评估,从前瞻性的角度来看,尽管在初始的阶段会有较高的时间、资源和预算投入,但是它会带来更低的总拥有成本。我们需要对其进行更细致的观察——在这方面花些工夫是值得的。
关于作者
Andrei Kaminski是 Softeq 企业级 Web 开发团队的一名.NET 开发人员,并且还是微软认证的专家,主要的关注点在于为复杂的业务问题寻找解决方案,在这个过程中会使用优雅的应用架构以及技术世界所提供的最好的东西。Kaminski 有多年实际的企业 Web 应用开发经验,包括 BPM、运营智能以及业务流程自动化方案。在他的职业兴趣列表中,CQRS 与事件溯源是排名第一的应用实现方案。
查看英文原文: CQRS for Enterprise Web Development: What’s in it for Business?
评论