使用只追加存储来记录对数据采取的完整系列操作,而不是仅存储域中数据的当前状态。 该存储可作为记录系统,可用于具体化域对象。 这样一来,无需同步数据模型和业务域,从而简化复杂域中的任务,同时可提高性能、可扩展性和响应能力。 它还可提供事务数据一致性并保留可启用补偿操作的完整审核记录和历史记录。
大多数应用程序会使用数据,而典型的方法是用户使用数据时通过更新数据使应用程序保持数据的当前状态。 例如,在传统的创建、读取、更新和删除 (CRUD) 模型中,典型的数据处理是从存储读取数据、对其作出修改、使用新值更新数据的当前状态(通常通过使用锁定数据的事务)。
CRUD 方法具有一些限制:
若要深入了解有关 CRUD 方法的限制,请参阅 CRUD, Only When You Can Afford It(仅在可承受一定限制的情况下使用 CRUD)。
事件溯源模式定义对一系列事件(每个事件记录在只追加存储中)驱动的数据进行处理操作的方法。 应用程序代码以命令方式描述每个数据操作的一系列事件发送到事件存储,这些事件在其中是持久化的。 每个事件表示对数据所作的一系列更改(例如 AddedItemToOrder
)。
事件在事件存储中持久化,事件存储充当数据当前状态的记录系统(权威数据源)。 事件存储通常会发布这些事件,使用者可收到通知并在需要时对其进行处理。 例如,使用者可启动将事件中的操作应用到其他系统的任务,或者执行完成此操作所需的任何关联操作。 请注意,生成事件的应用程序代码从订阅到事件的系统中分离。
事件存储发布的事件的典型用途是在应用程序中的操作更改实体时保持实体的具体化视图以及用于与外部系统集成。 例如,系统可保持用于填充 UI 各部分的所有客户订单的具体化视图。 应用程序添加新的订单、添加或删除订单中的项和添加发货信息时,可处理描述这些更改的事件以及使用这些事件来更新具体化视图。
此外,应用程序可随时读取事件历史记录,并通过播放和使用所有与实体相关的事件,使用事件历史记录来具体化实体的当前状态。可根据需要,在处理请求时或通过计划任务具体化域对象,将实体状态保存为具体化视图以支持演示层。
此图提供了此模式的概述,其中包括使用事件流的部分选项,例如创建具体化视图、将事件与外部应用程序和系统集成以及重播事件以创建特定实体的当前状态投影。
事件溯源模式具有以下优点:
通过执行响应事件的数据管理任务和具体化存储事件的视图,事件溯源通常与 CQRS 模式结合。
在决定如何实现此模式时,请考虑以下几点:
只有通过重播事件创建具体化视图或生成数据投影时,系统才可实现最终一致性。 应用程序将事件添加到事件存储作为处理请求的结果、发布事件和事件使用者处理事件之间存在一定程度的延迟。 在此期间,描述实体的进一步更改的新事件可能已到达事件存储。
备注
有关最终一致性的信息,请参阅 Data consistency primer(数据一致性入门)。
事件存储是信息的永久源,因此请勿更新事件数据。 更新实体以撤销更改的唯一方式是将补偿事件添加到事件存储。 如果持久化事件的格式(而不是数据)需要更改,也许在迁移期间,很难将存储中的现有事件和新版本结合。 可能需要循环访问所有事件进行更改,使其符合新格式,或添加使用新格式的新事件。 考虑在事件架构的每个版本上使用版本标记,以同时保留事件的旧格式和新格式。
多线程应用程序和应用程序的多个实例可能将事件存储在事件存储中。 事件存储中的事件一致性至关重要,影响特定实体的事件的顺序(实体更改发生的顺序会影响当前状态)同样至关重要。 将时间戳添加到每个事件有助于避免出现问题。 另一常见做法是使用增量标识符注释请求引起的每个事件。 如果两个操作尝试同时为同一实体添加事件,则事件存储可拒绝与现有实体标识符和事件标识符相匹配的事件。
读取事件以获取信息并没有标准方法或现有机制,例如 SQL 查询。 可提取的唯一数据是将事件标识符用作条件的事件流。 事件 ID 通常会映射到各个实体。 仅可根据实体原始状态通过重播与其关联的所有事件来确定实体的当前状态。
每个事件流的长度会影响管理和更新系统。 如果是大型流,请考虑按特定间隔(例如指定数量的事件)创建快照。 可通过快照和重播此时间点后发生的事件获取实体的当前状态。 有关创建数据快照的详细信息,请参阅 Martin Fowler 的企业应用程序体系结构网站上的快照和 Master-Subordinate Snapshot Replication(主从关系快照复制)。
即使事件溯源会最大程度降低数据更新冲突的可能性,应用程序仍必须能够处理由最终一致性和缺少事务引起的不一致性。 例如,在指示存货减少的事件到达数据存储时,客户可能正在对该商品下订单,这会导致需要在这两个操作之间作出协调,即通知客户或创建延期交付订单。
事件发布可能是“至少一次”,因此事件使用者必须是幂等的。 如果事件处理次数大于 1,则使用者不得重新应用该事件中描述的更新。 例如,如果使用者的多个实例将一个合计保留为实体的属性(例如已下订单总数),则下订单事件发生时,仅一个实例必须可成功增加合计。 尽管这不是事件溯源的主要特点,但却是通常的实施决策。
请在以下方案中使用此模式:
要捕获数据中的意图、用途或原因。 例如,可将对客户实体的更改捕获为一系列特定事件类型,例如“已搬家”、“帐户已关闭”或“已死亡”。
尽量减少或完全避免出现数据更新冲突。
需要记录发生的事件,并可重播事件以还原系统状态、回滚更改或保留历史记录和审核日志。 例如,任务涉及多个步骤时,可能需要执行操作来恢复更新,并重播某些步骤使数据重返一致的状态。
使用事件是应用程序操作的自然功能,且几乎不需要其他开发或实施工作。
需要将输入或更新数据的过程从应用这些操作所需的任务中分离。 为了提高 UI 性能或在事件发生时会事件分发到采取操作的其他侦听器。 例如,将工资系统与开支报销网站集成,使由事件存储引起的响应网站中数据更新的事件可同时供该网站和工资系统使用。
希望随要求更改而灵活更改具体化模型和实体数据的格式,或需要调整读取模型或公开数据的视图(与 CQRS 结合使用时)。
与 CQRS 结合使用且更新读取模型时最终一致性可接受或事件流中的解冻实体和数据的性能影响可接受。
此模式在以下情况中可能不起作用:
会议管理系统需要跟踪会议的已完成预订数,以检查潜在与会者预订时是否有可用席位。 此系统可通过至少两种方式存储会议的预订总数:
下图说明了如何使用事件溯源实施会议管理系统的席位预订子系统。
预订两个席位的操作顺序如下:
SeatAvailability
,且包含在公开此聚合中数据的查询和修改方法的域模型中。
需要考虑的一些优化是使用快照(使获取聚合的当前状态无需查询和重播事件的完整列表)和将此聚合的缓存副本保留在内存中。SeatAvailability
聚合会记录包含已预订席位数的事件。 聚合下次应用事件时,会使用所有的预订数来计算剩余的席位数。如果某位用户取消席位,此系统将执行相似过程,但命令处理程序会发出生成席位取消事件并将其追加到事件存储的命令。
除了扩大可伸缩性范围外,使用事件存储还可提供会议预订和取消预订的完整历史记录或审核线索。 事件存储中的事件是准确的记录。 无需以其他任何方式持久化聚合,因为此系统可轻松重播事件并将状态还原到任意时间点。