事件溯源模式

Azure

在仅追加存储中存储对对象采取的一系列作,而不是只存储关系数据库中数据的当前状态。 该存储可作为记录系统,可用于具体化域对象。 此方法可以提高复杂系统中的性能、可伸缩性和可审核性。

重要

事件溯源是一种复杂的模式,它渗透到整个体系结构中,并引入了权衡,以实现更高的性能、可伸缩性和可审核性。 系统成为事件溯源系统后,所有未来的设计决策都会受到这是事件溯源系统这一事实的约束。 迁移到或移出事件溯源系统的成本很高。 此模式最适合性能与可伸缩性是最高要求的系统。 事件溯源添加到系统的复杂性对于大多数系统来说并不合理。

上下文和问题

大多数应用程序使用数据,典型的方法是应用程序将数据的最新状态存储在关系数据库中,根据需要插入或更新数据。 例如,在传统的创建、读取、更新和删除(CRUD)模型中,典型的数据处理是从存储中读取数据、对数据进行一些修改,并使用新值更新数据的当前状态(通常通过使用锁定数据的事务)。

对于大多数方案,CRUD 方法非常简单且快速。 但是,在高负载系统中,此方法有一些挑战:

  • 性能:随着系统缩放,由于资源争用和锁定问题争用,性能会降低。

  • 可伸缩性:CRUD 系统是同步的,并且更新的数据作块。 这可能会导致系统负载不足时出现瓶颈和更高的延迟。

  • 可审核性:CRUD 系统仅存储数据的最新状态。 除非有一种审核机制记录单独日志中每个作的详细信息,否则历史记录将丢失。

解决方案

事件溯源模式定义对一系列事件(每个事件记录在只追加存储中)驱动的数据进行处理操作的方法。 应用程序代码引发事件,这些事件必须描述对对象执行的作。 事件通常发送到队列,其中单独的进程、事件处理程序、侦听队列并保留事件存储中的事件。 每个事件表示对对象的逻辑更改,例如 AddedItemToOrderOrderCanceled

事件在事件存储中持久化,事件存储充当数据当前状态的记录系统(权威数据源)。 其他事件处理程序可以侦听他们感兴趣的事件并采取适当的作。 例如,使用者可启动将事件中的操作应用到其他系统的任务,或者执行完成此操作所需的任何关联操作。 请注意,生成事件的应用程序代码从订阅到事件的系统中分离。

在任何时候,应用程序都可能读取事件的历史记录。 然后,可以使用事件通过播放和使用与该实体相关的所有事件来具体化实体的当前状态。 可根据需要在处理请求时执行此过程来具体化域对象。

由于读取和重播事件相对昂贵,因此应用程序通常实现 具体化视图、针对查询优化的事件存储的只读投影。 例如,系统可以维护用于填充 UI 的所有客户订单的具体化视图。 当应用程序添加新订单、添加或删除订单上的项或添加发货信息时,将引发事件,处理程序更新具体化视图。

该图显示了模式的概述,包括模式的典型实现,包括使用队列、只读存储、将事件与外部应用程序和系统集成,以及重播事件以创建特定实体当前状态的投影。

事件溯源模式概述和示例

工作流程

下面介绍了此模式的典型工作流:

  1. 呈现层调用负责从只读存储读取的对象。 返回的数据用于填充 UI。
  2. 呈现层调用命令处理程序来执行诸如创建购物车之类的作,或向购物车添加项目。
  3. 命令处理程序调用事件存储来获取实体的历史事件。 例如,它可以检索所有购物车事件。 这些事件在对象中播放,以在发生任何作之前具体化实体的当前状态。
  4. 运行业务逻辑并引发事件。 在大多数实现中,事件将推送到队列或主题,以分离事件生成者和事件使用者。
  5. 事件处理程序侦听他们感兴趣的事件,并为该处理程序执行相应的作。 一些典型的事件处理程序作包括:
    1. 将事件写入事件存储
    2. 更新针对查询优化的只读存储
    3. 与外部系统集成

模式优势

事件溯源模式具有以下优点:

  • 事件不可变,并且可使用只追加操作进行存储。 用户界面、工作流或启动事件的进程可继续,处理事件的任务可在后台运行。 此过程与处理事务期间没有争用的事实相结合,可以极大地提高应用程序的性能和可伸缩性,尤其是表示层。

  • 事件是描述已发生操作的简单对象以及描述事件代表的操作所需的相关数据。 事件不会直接更新数据存储。 只会对事件进行记录,以便在合适的时间进行处理。 使用事件可简化实现和管理。

  • 事件通常对域专家而言具有意义,然而对象关系阻抗不匹配却会让复杂数据库表变得难以理解。 表是表示系统的当前状态(而不是已发生事件)的人工构造。

  • 事件溯源不需要直接更新数据存储中的对象,因而有助于防止并发更新造成冲突。 但是,域模型必须仍然设计为避免可能导致不一致状态的请求。

  • 事件的只追加存储提供的审核线索可用于监视对数据存储采取的操作。 它可以通过随时重播事件将当前状态重新生成为具体化视图或投影,并且可以帮助测试和调试系统。 此外,使用补偿事件取消更改的要求可以提供反向更改的历史记录。 如果模型存储了当前状态,则此功能不会是这种情况。 事件列表还可用于分析应用程序性能和检测用户行为趋势。 或者,也可用于获取其他有用的业务信息。

  • 命令处理程序引发事件,任务执行作以响应这些事件。 通过将任务从事件中分离,可提供灵活性和可扩展性。 任务知道事件类型和事件数据,但不知道触发事件的操作。 此外,多个任务可以处理每个事件。 这样可实现与仅侦听事件存储引发的新事件的其他服务和系统的轻松集成。 但是,事件溯源事件的级别通常非常低,可能需要生成特定的集成事件。

事件溯源通常与 CQRS 模式 相结合,方法是执行数据管理任务以响应事件,并通过具体化存储事件的视图。

问题和注意事项

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

  • 最终一致性 - 仅当创建具体化视图或通过重播事件生成数据投影时,系统才会最终保持一致。 应用程序将事件添加到事件存储作为处理请求的结果、发布事件和事件使用者处理事件之间存在一定程度的延迟。 在此期间,描述实体的进一步更改的新事件可能已到达事件存储。 客户必须能够了解数据最终一致性这一事实,并且系统应设计为考虑到这些方案中的最终一致性。

    注意

    有关最终一致性的信息,请参阅 Data consistency primer(数据一致性入门)。

  • 版本控制事件 - 事件存储是信息的永久来源,因此不应更新事件数据。 更新实体或撤消更改的唯一方法是向事件存储添加补偿事件。 如果持久化事件的架构(而不是数据)需要更改(也许在迁移期间),则很难将存储中的现有事件与新版本合并。 应用程序需要支持对事件结构的更改。 这可以通过多种方法来实现。

    • 确保事件处理程序支持所有版本的事件。 这可以是维护和测试的挑战。 这需要在事件架构的每个版本上实现版本戳,以维护旧事件格式和新事件格式。
    • 实现事件处理程序来处理特定事件版本。 这可能是一个维护难题,在该 bug 修复更改可能需要跨多个处理程序进行。 这需要在事件架构的每个版本上实现版本戳,以维护旧事件格式和新事件格式。
    • 实现新架构时,将历史事件更新到新架构。 这会破坏事件的不可变性。
  • 事件排序 - 多线程应用程序和多个应用程序实例可能在事件存储中存储事件。 事件存储中的事件一致性至关重要,影响特定实体的事件的顺序(实体更改发生的顺序会影响当前状态)同样至关重要。 将时间戳添加到每个事件有助于避免出现问题。 另一常见做法是使用增量标识符注释请求引起的每个事件。 如果两个操作尝试同时为同一实体添加事件,则事件存储可拒绝与现有实体标识符和事件标识符相匹配的事件。

  • 查询事件 - 没有标准方法或现有机制(例如 SQL 查询),用于读取事件以获取信息。 可提取的唯一数据是将事件标识符用作条件的事件流。 事件 ID 通常会映射到各个实体。 仅可根据实体原始状态通过重播与其关联的所有事件来确定实体的当前状态。

  • 重新创建实体状态的成本 - 每个事件流的长度会影响管理和更新系统。 如果是大型流,请考虑按特定间隔(例如指定数量的事件)创建快照。 可通过快照和重播此时间点后发生的事件获取实体的当前状态。 有关创建数据快照的详细信息,请参阅主-从快照复制

  • 冲突 - 即使事件溯源最大限度地减少了数据更新冲突的可能性,应用程序仍必须能够处理最终一致性和缺少事务导致的不一致。 例如,在指示存货减少的事件到达数据存储时,客户可能正在对该商品下订单。 这种情况导致需要在这两个操作之间作出协调,即通知客户或创建延期交付订单。

  • 需要幂等性 - 事件发布 可能至少为一次,因此事件使用者必须是幂等的。 如果事件处理次数大于 1,则使用者不得重新应用该事件中描述的更新。 使用者 cn 的多个实例维护并聚合实体的属性,例如已下订单总数。 下订单事件发生时,只有一个实例必须成功递增聚合。 尽管这个结果不是事件溯源的主要特点,但却是通常的实现决策。

  • 循环逻辑 - 请注意处理一个事件涉及创建一个或多个新事件的情况,因为这可能会导致无限循环。

何时使用此模式

请在以下方案中使用此模式:

  • 要捕获数据中的意图、用途或原因。 例如,可将对客户实体的更改捕获为一系列特定事件类型,例如已搬家帐户已关闭已身故

  • 尽量减少或完全避免出现数据更新冲突。

  • 需要记录发生的事件,并重播事件以还原系统状态、回滚更改或保留历史记录和审核日志。 例如,任务涉及多个步骤时,可能需要执行操作来恢复更新,并重播某些步骤使数据重返一致的状态。

  • 使用事件时。 这是应用程序操作的自然功能,且几乎不需要其他开发或实现工作。

  • 需要将输入或更新数据的过程从应用这些操作所需的任务中分离。 此更改可能是为了提高 UI 的性能,或者是为了将事件分发给其他在事件发生时采取操作的侦听器。 例如,可以将工资管理系统与开支报销网站集成。 由事件存储引发的用于响应网站中数据更新的事件可同时供该网站和工资管理系统使用。

  • 希望随要求更改而灵活更改具体化模型和实体数据的格式,或需要调整读取模型或公开数据的视图(与 CQRS 结合使用时)。

  • 与 CQRS 结合使用且更新读取模型时最终一致性可接受或事件流中的解冻实体和数据的性能影响可接受。

此模式在以下情况中可能不起作用:

  • 不需要超大规模或性能的应用程序。

  • 小型域或简单域、几乎或完全没有业务逻辑的系统或者自然地适用于传统 CRUD 数据管理机制的非域系统。

  • 要求一致性和数据视图实时更新的系统。

  • 基础数据更新冲突发生率低的系统。 例如,主要是添加数据而不是更新数据的系统。

工作负荷设计

架构师应评估如何在其工作负荷的设计中使用“事件溯源模式”,以解决 Azure Well-Architected Framework 支柱中涵盖的目标和原则。 例如:

支柱 此模式如何支持支柱目标
可靠性设计决策有助于工作负荷在发生故障后复原,并确保它在发生故障后恢复到正常运行状态。 由于捕获复杂业务流程中更改的历史记录,如果需要恢复状态存储,它可以促进状态重建。

- RE:06 数据分区
- RE:09 灾难恢复
性能效率通过在缩放、数据和代码方面进行优化, 帮助工作负载高效地满足需求 此模式通常与 CQRS(适当的域设计和战略快照处理)相结合,可以提高工作负载性能,因为它采用了原子的仅追加操作,并且避免了对写入和读取的数据库锁定。

- PE:08 数据性能

与任何设计决策一样,请考虑对可能采用此模式引入的其他支柱的目标进行权衡。

示例

会议管理系统需要跟踪会议的已完成预订数。 这种方式可以检查潜在与会者预订时是否有可用席位。 此系统可通过至少两种方式存储会议的预订总数:

  • 此系统可将预订总数信息作为单独的实体存储在包含预订信息的数据库中。 进行预订或取消预订时,此系统可相应地增加或减少此数量。 理论上而言,此方式很简单,但如果短时间内有大量与会者尝试预订席位,则可能导致可伸缩性问题。 例如,在预订期结束前的最后一天左右。

  • 此系统可将预订和取消预订信息存储为事件存储中的事件。 可通过重播这些事件来计算可用的席位数。 由于事件的不变性,此方式更具伸缩性。 此系统仅需要可从事件存储读取数据,或将数据追加到事件存储。 不会修改有关预订和取消预订的事件信息。

下图说明了如何使用事件溯源实施会议管理系统的席位预订子系统。

使用事件溯源捕获会议管理系统中有关席位预订的信息

预订两个席位的操作顺序如下:

  1. 用户界面发出为两位与会者预订席位的命令。 该命令由单独的命令处理程序处理。 一条逻辑,此逻辑从用户界面分离且负责处理发布为命令的请求。

  2. 通过查询描述预订和取消的事件来构造包含有关会议的所有预留信息的实体。 此实体被调用 SeatAvailability,并包含在一个域模型中,该模型公开用于查询和修改实体中的数据的方法。

    要考虑的一些优化是使用快照(因此无需查询和重播事件的完整列表以获取实体的当前状态),并在内存中维护实体的缓存副本。

  3. 命令处理程序调用域模型公开的方法来进行预订。

  4. SeatAvailability 实体引发包含保留的席位数的事件。 下次实体应用事件时,将使用所有预留来计算剩余席位数。

  5. 此系统将新事件追加到事件存储中的事件列表。

如果某位用户取消席位,此系统将执行相似过程,但命令处理程序会发出生成席位取消事件并将其追加到事件存储的命令。

除了扩大可伸缩性范围外,使用事件存储还可提供会议预订和取消预订的完整历史记录或审核线索。 事件存储中的事件是准确的记录。 无需以其他任何方式持久化聚合,因为此系统可轻松重播事件并将状态还原到任意时间点。

后续步骤

实施此模式时,可能也会与以下模式和指南相关:

  • 命令和查询责任分离 (CQRS) 模式。 提供 CQRS 实现的信息永久源的写入存储通常基于事件溯源模式的实现。 介绍使用独立接口将读取应用程序中数据的操作与更新数据的操作分离。

  • 具体化视图模式。 基于事件溯源的系统所使用的数据存储通常不是很适合高效查询。 常见做法是定期或在数据更改时生成数据的预填充视图。

  • 补偿事务模式。 系统不会更新事件溯源存储中的现有数据。 而是会添加将实体状态转换到新值的新条目。 若要撤销更改,需使用补偿条目,因为不可能撤销上一个更改。 介绍如何撤销上一个操作执行的工作。