在没有事件溯源的情况下使用CQRS


CQRS和Event Sourcing都是架构设计中的强大构建块,但它们也增加了复杂性,可能并不适合所有情况。因此,如果您想构建基于CQRS的体系结构,那么了解基于事件源的持久性的替代方案是有益的。
Stackoverflow上的一些博客文章和帖子将CQRS和事件采购称为“正交概念”,它们可以相互独立地应用。但是,大多数文章和示例都将这两个概念融合到一个基于事件的体系结构中,而没有讨论任何解耦方法。解耦它们正是本文的内容。
您想使用CQRS,但对事件溯源的影响犹豫不决?您是否想知道如何省略事件源可能会影响基于CQRS的架构?如果这个问题的答案是“是”,那么这篇文章可能对您有意义。

CQRS的本质
CQRS指出应用程序架构可分为两部分:

  • 写入端:通过执行命令修改应用程序状态的部分。
  • 读取端:通过执行查询读取应用程序状态的部分。

这两个问题都是相互独立实现的。隔离的程度取决于具体的架构设计。可以将它应用于系统的逻辑层,并让双方共享同一个数据库。但通常,隔离会扩展到数据库级别。在这种情况下,写操作和读操作适用于不同的数据库。
就像在写入端和读取端隔离应用程序一样,然后会在写入存储区和读取存储区中隔离持久层:
  • 写入状态存储数据库是作为事实真实的源,它包含当前应用程序状态或包含重建它所需的所有信息。因此,写存储被优化以实现数据完整性并维持正确的应用状态。使用典型的RDBMS方式,可以通过数据规范化,完整性检查和事务来实现。保持应用程序状态干净的另一种方法是简单地修改它。这是Event Sourcing模式采用的方法:状态表示为一种事件序列,永远不会修改或删除现有数据。这很难搞砸现有数据。
  • 阅读状态存储数据库是由查询使用的数据库和面向数据消费者的需求量身定做。这可能包括非规范化,聚合或分层数据。相同的数据也可以以冗余方式存储,以满足不同的表示需求。

两个数据库之间需要同步。写入存储的每个修改都应该反映在读取存储中。此步骤称为数据投影。投影可以以各种方式实现,并且极大地影响应用程序架构。特别是,对投影的选择与使用事件溯源的决定密切相关。因此,接下来的部分将详细介绍不同的实施策略。

基于事件的投影
大多数CQRS实现利用事件流作为数据投影的触发器。这是一种优雅的方法,因为事件自然会描述应用程序状态更改。大多数(分布式)系统已经以某种方式处理它们。处理应用程序状态更改通常涉及以下步骤:

  • 对象实例化一个命令,该命令表示导致某个应用程序状态更改的操作。
  • 该命令被分派到命令处理程序,该命令处理程序验证命令并触发一些域逻辑运行。
  • 领域逻辑如果修改应用程序状态就发出一个或多个不可变事件对象,描述刚刚发生的状态更改。
  • 事件被分派给适当的事件处理程序,后者为每个事件更新读取数据库。事件通常以最终一致的方式异步处理。

基于事件的投影并不一定意味着需要事件源持久性方法,但有一些事实使存储到数据库成为一个相当明显的选择:
  • 基于事件的投影需要持久的事件。投影并不总是顺利。可能存在一些数据库连接问题或事件处理程序实现中的错误。最终,一些投影会失败。因此,系统需要一种重放事件投影,这就需要持久的事件存储。
  • 拥有除事件存储之外的写数据库需要(分布式)事务。如果我们将事件保存在事件存储中,但另外将当前应用程序状态存储在数据库中,该怎么办?例如,我们是否可以将所有实体的当前状态放在关系数据库中,同时在另一个事件存储中存储事件历史记录?我们可以轻松加载实体状态而不会触及任何事件,并且仍然可以在需要时重放投影。 问题是,两个写操作都必须作为原子操作来处理:为了确保数据完整性,写入数据库和事件存储必须成功或两者都失败。除非两个写入存储使用相同的数据库技术并驻留在同一数据库中,否则需要分布式事务来保证数据完整性。虽然有这个问题的解决方案,处理写操作显着变得更加复杂。相反,当使用事件溯源时,这个问题根本不存在。
  • 多个写入存储数据库更难管理。在事件存储数据库旁边使用附加写入存储数据库也会增加数据库管理的复杂性。由于两个数据库都构成了真相的来源,因此它们必须同步。在这种情况下,这意味着两个数据库必须反映已将完全相同的命令序列写入每个相应数据库的状态。在定义备份/恢复策略或手动数据更正时,这会很有趣。

当使用基于事件的投影时,使持久的事件序列成为事实的唯一来源是很有意义的。这正是Event Sourcing的作用。基于事件的投影选择使CQRS和事件采购如此匹配。在这样的系统中,CQRS和事件源很难称为“正交概念”,因为没有事件源,实现可靠的基于事件的投影将变得非常困难。

因此,如果您想在没有事件溯源的情况下使用CQRS,还有哪些替代方案? 从更抽象的角度来看,基于事件的投影是一种基于小增量的投影策略。由于任何投影都需要重播,任何此类策略都需要将这些增量作为真相的来源。因此,替代投影策略根本不会基于有关增量的信息,而是基于当前的应用状态。

基于状态的投影
基于状态的投影不是投射小的增量,而是作为一个整体对实体起作用。有不同的实施策略:
数据库视图:
使用关系数据库时,基于CQRS的系统的读取端可以对数据库视图进行操作。然后,投影完全由数据库本身处理。将SQL作为查询语言,这种方法在数据聚合和转换方面提供了很大的灵活性。使用像Dapper这样的Micro-ORM框架,可以轻松实现轻薄的读取端,同时充分利用CQRS模式。但是,使用数据库视图时,对于分层数据结构,非规范化是有限的。

基于实体的投影处理程序:
在基于事件的投影中,投影逻辑使用一组事件处理程序来实现。以类似的方式,可以使用基于实体的投影处理程序来实现基于状态的投影逻辑:

interface IEntityProjection<TEntity>
{
    void Project(TEntity entity);
}

每当在写入侧改变类型T的实体时,需要触发类型T的投影处理程序。在较大的系统中,这可以以最终一致的方式异步完成。可以通过设置基于行的脏标志或投影引擎观察到的行版本号来触发投影。
实现基于状态的投影不需要事件或事件处理程序。因此,如果您想构建一个不使用事件源的基于CQRS的系统,这种方法很有效。当然,这并不一定意味着系统设计中没有事件或事件处理程序。例如,您仍可能希望在与系统组件之间的通信相关的方案中使用它们。