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