建立事件存储

2020-12-13 03:37:02

在“事件作为存储机制”中,从概念的角度考察了从一系列事件重建状态的概念。本章将重点讨论实际事件存储的实现以及在产生实现时会出现的一些问题。

本章中讨论的实现并非旨在成为具有生产质量的事件存储,更多地是作为围绕如何构建事件存储的讨论点而提供的。尽管性能不高,但此处的实现可以满足当今构建的大部分应用程序的需求。

对于说明性实施,最简单的方法是在现有技术(如RDBMS)中构建事件存储。这将减轻可能出现的许多技术问题,这些问题不在有关如何构建事件存储的基本讨论的范围内,例如事务提交模型或数据局部性以提高读取性能。

基本事件存储可以仅使用两个表在关系数据库中表示。

该表代表实际的事件日志。该表中的每个事件只有一个条目。事件本身存储在[Data]列中。该事件是使用某种形式的序列化存储的,对于本讨论的其余部分,该机制将假定是建立在序列化中的,尽管使用记忆模式可能会非常有利。

该表显示的信息量最少,大多数组织希望添加几列,例如进行更改的时间或与更改关联的上下文信息。上下文信息的示例可能包括发起更改的用户,他们从中发出更改的IP地址或他们在发出更改时的权限级别。

版本号也与每个事件一起存储在事件表中。在大多数情况下,通常可以将其视为一个递增的整数。保存的每个事件都有一个递增的版本号。版本号仅在给定聚合的上下文中是唯一且顺序的。这是因为聚合根边界是一致性边界。

[AggregateId]列是应该建立索引的外键;它指向下一个表,即“聚集”表。

作者评论:我在事件存储中称呼这个概念为“聚合”来代替诸如“事件提供者”之类的另一个名字,因为“聚合”实际上是一个域概念,并且事件存储可以在没有域的情况下工作。

聚合表代表当前系统中的聚合,每个聚合必须在此表中都有一个条目。除标识符外,还对当前版本号进行了非规范化。这主要是一种优化,因为它可以从事件表中派生,但是查询非规范化要比直接查询事件表要快得多。此值还用于乐观并发检查中。

在此示例中还包括[Type]列,这将是要存储的聚合类型的完全限定名称。这对于各种目的很有用,其中不仅包括调试,而且对于创建基本事件存储来说是不必要的。

与大多数数据存储机制相比,事件存储要简单得多,因为它们不支持通用查询。最简单的事件存储只有两个操作。仅执行两个操作就使事件存储比大多数数据存储机制更简单,并且更易于优化。

第一步是获取所有事件的汇总。按事件写入的顺序对事件进行排序非常重要,可以将版本号用于此目的。使用底层RDBMS可以很简单地完成所有操作。

这是生产系统应针对事件存储执行的唯一查询。可能有用的辅助查询是通过实际日期限制此结果集,以便在某个时间点查看对象的状态,但通常生产系统不应这样做。

事件存储必须支持的另一种操作是将一组事件写入聚合根。这可以用代码或存储过程来完成。如果没有插入过程,则首选包含if语句的存储过程或动态生成的SQL将需要多次往返。清单1中可以看到插入过程的伪代码。

尽管其中有一些细微之处,但写操作也相对简单。基本叙述是,它首先检查是否存在带有要使用的唯一标识符的聚合,如果没有,它将创建该聚合并将其视为当前版本。然后,如果预期版本与实际版本不匹配,它将尝试对传入的数据进行乐观并发测试,这将引发并发异常。如果版本相同,则它将循环浏览所保存的事件并将其插入事件表中,从而为每个事件将版本号增加一。最后,它将“聚合”表更新为该聚合的新的当前版本号。重要的是要注意,这些操作是在事务中,因为需要确保乐观的并发性在分布式环境中起作用。

可以使用以下接口定义代码中事件存储的合同。

尽管创建产品质量的事件存储并非轻而易举的事,但事件存储背后的总体概念相对容易。将来可能会有许多现成的事件存储系统作为产品或开源项目提供。但是,“事件作为存储机制”中讨论了一个非常重要的优化,它实际上应该存在于大多数系统中,这就是“滚动快照”的概念。

滚动快照是一种启发式方法,可以防止在发出查询以重建聚合时加载所有事件。它们是在给定时间点的聚合异常化。将启发式方法添加到基本事件存储中,只需更改查询逻辑和其他表即可。在概念级别上有关滚动快照的进一步讨论可以在“事件作为存储机制”一章中找到。

快照表是相对基本的。它是Blob中的主要数据,其中包含给定时间点的汇总的序列化版本。序列化的数据可以是许多可能的模式,二进制,XML,原始文本等中的任何一种。如何序列化快照的决定实际上取决于所构建的系统。快照包含一个版本号,它表示快照代表的聚合的哪个版本。

为了创建快照,需要引入处理快照创建任务的过程。此过程可以作为后台进程驻留在Application Server外部。由于吞吐量的关系,可以运行一个进程,也可以根据需要运行多个进程。所有快照都是异步发生的。图4显示了引入[SnapShotter]流程的概念架构。

[SnapShotter]位于事件存储后面,并定期查询所有需要快照的聚合,因为它们已经超过了允许的事件数量。通过将Aggregates表连接到Aggregate标识符上的Snapshots表,可以在讨论的简单事件存储中轻松地完成此查询。差异是通过使用where子句从当前版本中减去最后一个快照版本而得出的,该子句仅返回差异大于某个数字的聚合。该查询将返回要创建快照的所有聚合。然后,快照程​​序将遍历此聚合列表以创建快照(如果使用多个快照程序,则竞争的消费者模式在此效果很好)。

创建快照的过程涉及让域加载聚合的当前版本,然后对其进行快照。快照的创建可以通过多种方式完成。拍摄快照后,会将其保存回快照表,以便查询可以使用快照。

尽管使用Memento模式在处理快照时非常有用,但许多人使用其平台上可用的默认序列化程序包都能获得良好的结果。随着域对象结构的变化,随着时间的推移,Memento模式(或自定义序列化)可以更好地隔离域。释放新结构时,默认的序列化程序存在版本控制问题(必须删除并重新创建或更新现有快照,以匹配新架构)。使用Memento模式可以将快照架构的版本与域对象本身分开。

在“事件作为存储机制”中,展示了一种不同的,更简单的快照存储机制。该系统的快照在事件日志中排成一行,这是另一种机制,尽管从概念上讲比较简单,但生产系统中可能会遇到一些问题。问题围绕着在事件日志中对快照进行排序的需求。

考虑到Snapshotter已经意识到聚合根需要创建快照。它加载聚合并拍摄快照。不幸的是,在执行此操作时,其中一个应用程序服务器对同一聚合进行了更改。由于快照在事件日志中取决于位置,因此它将收到乐观并发故障。简单的答案是简单地重复该过程,但是如果再次失败该怎么办?繁忙的聚合服务器上的快照程序可能会最终成功写入快照的可能性很小。

通过将快照分成各自的表并将它们与聚合的版本关联,可以解决此问题。不需要对快照进行排序,快照甚至不需要是最新版本,所拍摄的快照在所拍摄的版本中有效。

快照是一种启发式方法,可以显着提高许多系统的性能,尽管并非所有系统都需要快照。通常建议在不使用快照的情况下处理开发,因为以后可以将其作为对系统的简单性能增强而引入。

先前已经讨论过,来自域的事件也是[集成模型]。通常,这些事件不仅被保存,而且还被发布到队列中,在该队列中,它们被异步地分发给同一系统内的侦听器(报告模型是一个很好的例子)或其他应用程序。许多发布事件的系统都存在一个问题,即它们需要在使用的任何存储(关系存储或其他存储)与事件发布到队列之间进行两阶段提交。

之所以需要两阶段提交,是因为在对数据存储的写入提交与对队列的写入提交之间的一小段时间内,可能会发生灾难。如果在此期间发生故障,则该消息将不会在队列中发布(或者如果是其他方向,则消息可能会发布,但更改可能不会保存)。如果发生任何一种情况,事件的侦听器将与生产者不同步。

两阶段提交可能很昂贵,但是对于低延迟系统,在处理这种情况时存在更大的问题。通常,队列本身是持久性的,因此该事件在两阶段提交中两次写入磁盘,一次写入事件存储,一次写入持久性队列。对于大多数具有双重写入功能的系统而言,这并不重要,但是如果您对延迟的要求较低,那么它可能会成为一项非常昂贵的操作,因为它还会强制在磁盘上进行寻道。图5说明了数据存储和发布队列之间的两阶段提交。

一些尝试通过仅写入队列来解决此问题,然后在队列的另一端使用事件表示的更改来更新数据存储的方法,但这存在一些问题。最大的问题是,并非所有事件都可以写入存储,最终引入了一致性,并且在写入事件时可能会出现乐观的并发问题。在生产系统中处理此问题并非易事。

许多组织则相反,将事件存储用作队列。在前面讨论的“事件”表中添加序列号可以将“事件存储”用作队列。图5说明了对事件表的架构的更改。