事件源模式

【博文目录>>>】


事件源模式

使用仅追加存储来记录描述在域中对数据执行的操作的完整事件序列,而不是仅存储当前状态,以便可以使用该存储来实现域对象。这种模式可以通过避免同步数据模型和业务域的要求,简化复杂领域中的任务;提高性能、可伸缩性和响应性;为事务性数据提供一致性;并维护完整的审计追踪和历史记录,以支持采取补偿措施。

背景与问题

大多数应用程序使用数据,并且典型的方法是应用程序在用户使用数据时通过更新数据来维护数据的当前状态。例如,在传统的创建,读取,更新和删除(CRUD)模型中,典型的数据处理将是从存储中读取数据,对其进行一些修改,并使用新值更新数据的当前状态,通常使用事务锁定数据。

CRUD方法有一些局限性:

  • CRUD系统直接针对数据存储执行更新操作这一事实可能会影响性能和响应能力,并由于所需的处理开销而限制可伸缩性。
  • 在与多个并发用户协作的域中,数据更新冲突更有可能发生,因为更新操作发生在单个数据项上。
  • 除非有额外的审核机制,将每个操作的细节记录在单独的日志中,否则历史记录就会丢失。


要更深入地了解CRUD方法的局限性,请参见MSDN上“CRUD,只有在您能够负担的情况下”

解决方案

事件源模式定义了一种处理由一系列事件驱动的数据操作的方法,每个事件都记录在仅附加的存储中。应用程序代码将一系列事件发送到事件存储区,这些事件必须描述数据上发生的每个操作,在事件存储中它们被持久化。每个事件表示对数据的一组更改(如AddedItemToOrder)。

事件将持久化在事件存储区中,该事件存储充当有关数据当前状态的真相或记录系统的源(给定数据元素或信息的权威数据源)。事件存储通常发布这些事件,以便通知消费者并在需要时处理它们。例如,消费者可以启动将事件中的操作应用于其他系统的任务,或者执行完成操作所需的任何其他相关操作。注意,生成事件的应用程序代码与订阅事件的系统分离。

事件存储发布的事件的典型用途是维护实体的物化视图,因为应用程序中的操作会改变它们,并与外部系统集成。例如,系统可能维护用于填充UI部分的所有客户订单的物化视图。当应用程序添加新订单、添加或删除订单上的项以及添加发送信息时,可以处理描述这些更改的事件,并使用这些事件更新物化视图。


有关更多信息,请参见物化视图模式

此外,在任何时候,应用程序都有可能读取事件的历史,并通过有效地“回放”并消耗与该实体相关的所有事件来实现实体的当前状态。这可能发生在需要时,以便在处理请求时实现域对象,或者通过计划的任务来实现,以便将实体的状态存储为支持表示层的物化视图。

图1显示了该模式的逻辑概述,包括一些使用事件流的选项,例如创建物化视图、将事件与外部应用程序和系统集成,以及重播事件以创建特定实体当前状态的预测。

在这里插入图片描述

图1-事件源模式的概述和示例

事件源模式提供了许多优点,包括:

  • 事件是不可变的,因此可以使用仅附加的操作来存储.发起产生事件的操作的用户界面、工作流或进程可以继续,处理事件的任务可以在后台运行。这与事务执行过程中不存在争用的事实相结合,可以极大地提高应用程序的性能和可伸缩性,特别是在表示级别或用户界面上。
  • 事件是描述发生的某些操作的简单对象,以及描述事件表示的操作所需的任何关联数据。事件不直接更新数据存储;它们只是在适当的时间被记录下来以便处理。这些因素可以简化实施和管理。
  • 事件通常对领域专家有意义,而对象-关系阻抗不匹配的复杂性可能意味着领域专家可能无法清楚地理解数据库表。表是表示系统当前状态的人工构造,而不是所发生的事件。
  • 事件来源可以帮助防止并发更新导致冲突,因为它避免了直接更新数据存储中的对象的要求。但是,域模型仍然必须设计为保护自身不受可能导致不一致状态的请求的影响。
  • 仅附加的事件存储提供了审计跟踪,可用于监视针对数据存储所采取的操作,通过在任何时候重播事件重新生成当前状态为物化视图或投影,并协助测试和调试系统。此外,使用补偿事件取消更改的要求提供了反向更改的历史记录,如果模型只是存储当前状态,则不会发生这种情况。事件列表还可用于分析应用程序性能和检测用户行为趋势,或获取其他有用的业务信息。
  • 事件与执行操作以响应事件存储引发的每个事件的任何任务之间的分离提供了灵活性和可扩展性。例如,处理事件存储引发的事件的任务只知道事件的性质及其包含的数据。任务的执行方式与触发事件的操作分离。此外,多个任务可以处理每个事件。这可能使您能够轻松地与只需要侦听事件存储引发的新事件的其他服务和系统集成。然而,事件源事件往往级别很低,可能需要生成特定的集成事件。


通过执行响应事件的数据管理任务,以及通过从存储的事件中物化视图,事件源通常与CQRS模式相结合。

问题和考虑

在决定如何实现此模式时,请考虑以下几点:

  • 只有在创建物化视图或通过重播事件生成数据投影时,系统才会最终保持一致。在应用程序由于处理请求而将事件添加到事件存储区、发布的事件和处理事件的使用者之间存在一些延迟。在此期间,描述对实体的进一步更改的新事件可能已到达事件存储区。


数据一致性原则有关最终一致性的信息。

  • 事件存储是不可变的信息源,因此永远不应该更新事件数据。要撤消更改,更新实体的唯一方法是添加补偿事件对于事件存储,就像您在会计中使用负事务一样。如果持久化事件的格式(而不是数据)需要更改(可能是在迁移期间),则很难将存储中的现有事件与新版本结合起来。可能需要迭代所有进行更改的事件,以便它们符合新格式,或者添加使用新格式的新事件。考虑在事件模式的每个版本上使用版本号,以便同时维护旧的和新的事件格式。

  • 多线程应用程序和多个应用程序实例可能在事件存储中存储事件。事件存储中事件的一致性至关重要,影响特定实体的事件顺序也是如此(对实体的更改顺序影响其当前状态)。向每个事件添加时间戳是一种可以帮助避免问题的选择。另一个常见的做法是用增量标识符对请求产生的每个事件进行注释。如果两个操作试图同时为同一实体添加事件,则事件存储可以拒绝与现有实体标识符和事件标识符匹配的事件。

  • 没有标准的方法,也没有现成的机制,比如SQL查询,来读取事件来获取信息。唯一可以提取的数据是使用事件标识符作为标准的事件流。事件ID通常映射到各个实体。实体的当前状态只能通过根据实体的原始状态重放与其相关的所有事件来确定。

  • 每个事件流的长度会对系统的管理和更新产生影响。如果流很大,请考虑按特定间隔创建快照,例如指定数量的事件。可以从快照中获取实体的当前状态,并通过重播在该时间点之后发生的任何事件来获得该实体的当前状态。


    有关创建数据快照的更多信息,请参阅Martin Fowler的企业应用程序体系结构网站上的快照和MSDN上的主从快照复制

  • 即使事件源将数据更新冲突的可能性降到最低,应用程序仍必须能够处理由于最终一致性和事务缺乏而可能出现的不一致。例如,在为该项目下订单时,可能会发生表明库存减少的事件出现在数据存储中,从而需要协调这两个操作;可能是通过通知客户或创建后台订单。

  • 事件发布可能是“至少一次”,因此事件的使用者必须是幂等的。如果事件不止一次被处理,则它们不能重新应用事件中描述的更新。例如,如果一个使用者的多个实例维护某个实体的属性的聚合(例如所下订单的总数),则在发生“Order Led”事件时,只有一个实例必须成功地递增该聚合。虽然这不是事件来源的固有特征,但这是通常的实现决策。

何时使用此模式

这种模式非常适合以下情况:

  • 当您想在数据中捕获“意图”、“目的”或“原因”时。例如,对客户实体的更改可能被捕获为一系列特定的事件类型,例如搬回家, 已结清帐户,或亡者.
  • 当减少或完全避免发生数据的冲突更新时是至关重要的。
  • 当您想要记录发生的事件,并且能够重播它们以恢复系统的状态时;使用它们回滚对系统的更改;或者简单地作为历史记录和审计日志。例如,当一个任务涉及多个步骤时,您可能需要执行操作来恢复更新,然后重放一些步骤以使数据恢复到一致的状态。
  • 当使用事件是应用程序操作的一个自然特性,并且几乎不需要额外的开发或实现工作。
  • 当您需要将输入或更新数据的过程与应用这些操作所需的任务分离时。这可能是为了提高UI性能,或者将事件分发给其他侦听器,例如当事件发生时必须采取某些操作的其他应用程序或服务。一个例子是将一个发薪系统与一个费用提交网站相结合,以便该网站和发薪系统使用由事件存储为响应在费用提交网站中进行的数据更新而引发的事件。
  • 如果需求发生变化,您希望灵活地更改物化模型和实体数据的格式,或者–当与CQRS一起使用时–您需要调整已读取的模型或公开数据的视图。
  • 当与CQRS一起使用时,在更新读取模型时,最终的一致性是可以接受的,或者,或者,在再水化实体和事件流的数据中所产生的性能影响是可以接受的。

这种模式可能不适用于以下情况:

  • 小或简单的域,很少或没有业务逻辑的系统,或与传统CRUD数据管理机制正常工作的非域系统。
  • 需要对数据视图进行一致性和实时更新的系统。
  • 不需要审计跟踪、历史记录和回滚和重放操作功能的系统。
  • 对底层数据进行冲突更新的情况非常少的系统。例如,主要是添加而不是更新数据的系统。

示例

会议管理系统需要跟踪已完成的会议预订数量,以便在潜在与会者尝试进行新预订时检查是否仍有座位可用。该系统可以至少以两种方式存储会议的预订总数:

  • 系统可以将有关预订总数的信息作为一个单独的实体存储在一个保存预订信息的数据库中。随着预订的进行或取消,系统可以酌情增加或减少此数目。这种方法在理论上很简单,但如果大量与会者试图在短时间内预订座位,则可能会导致可伸缩性问题。例如,在最后一天左右的预订期结束之前。
  • 系统可以将有关预订和取消的信息存储为在事件存储中举行的事件。然后,它可以通过重播这些活动来计算可用的座位数。由于事件的不可变性,这种方法可以更具有可伸缩性。系统只需要能够从事件存储区读取数据,或者将数据附加到事件存储区。有关预订和取消的事件信息从不修改。

图2显示了如何使用事件源实现会议管理系统的座位预订子系统。

在这里插入图片描述

图2-使用事件源捕获会议管理系统中有关座位预订的信息

保留两个席位的行动顺序如下:

  1. 用户界面发出命令为两个与会者保留座位。该命令由一个单独的命令处理程序处理(一段逻辑与用户界面分离,负责处理作为命令发布的请求)。
  2. 一个包含会议所有预订信息的汇总是通过查询描述预订和取消的事件来构建的。这个聚合称为可通过性,并且包含在一个域模型中,该模型公开用于查询和修改聚合中的数据的方法。


需要考虑的一些优化是使用快照(这样您就不需要查询和重播完整的事件列表来获取聚合的当前状态),并在内存中维护聚合的缓存副本。

  1. 命令处理程序调用域模型公开的方法来进行保留。
  2. 这个可通过性汇总记录包含保留席位数目的事件。下一次,当总票数适用于各种活动时,所有保留的席位将被用来计算剩余席位的数量。
  3. 系统将新事件附加到事件存储中的事件列表中。

如果用户希望取消座位,系统将遵循类似的处理程序,只是命令处理程序发出生成取消座位事件的命令,并将其附加到事件存储区。

除了为可伸缩性提供更多的空间外,使用事件存储还提供了会议预订和取消的完整历史记录或审计跟踪。事件存储中记录的事件是确定的,也是唯一的真相来源。不需要以任何其他方式持久化聚合,因为系统可以轻松地重放事件并将状态恢复到任何时间点。


您可以在MSDN上的模式和实践指南CQRS Journey中的介绍事件源一章中找到有关此示例的更多信息。

相关模式和指导

在实施这一模式时,下列模式和指导也可能相关:

  • 命令和查询责任隔离(CQRS)模式。为CQRS实现提供不可变信息源的写存储区通常基于事件源模式的实现。命令和查询责任隔离模式描述了如何使用单独的界面将读取应用程序中数据的操作与更新数据的操作隔离。
  • 物化视图模式。基于事件源的系统中使用的数据存储通常不太适合高效查询。取而代之的是,一种常见的方法是定期或在数据更改时生成数据的预填充视图。物化视图模式显示了如何实现这一点。
  • 补偿交易模式。事件源存储中的现有数据不会更新;而是添加了新条目,这些条目将实体的状态转换为新值。要撤消更改,将使用补偿条目,因为不可能简单地撤消先前的更改。补偿事务模式描述了如何撤消上一个操作所执行的工作。
  • 数据一致性入门。将事件源与单独的读取存储区或实例化视图一起使用时,读取的数据将不会立即保持一致;相反,它只会最终保持一致。数据一致性入门概述了有关维护分布式数据一致性的问题。
  • 数据分区指南。使用事件源时,通常会对数据进行分区,以提高可伸缩性,减少争用并优化性能。数据分区指导指南介绍了如何将数据划分为离散的分区以及可能出现的问题。

更多信息

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章