September 2015

第 30 卷,第 9 期

最前沿 - 适合常见应用程序的事件源

作者 Dino Esposito | September 2015

Dino Esposito这是一种自然发生的常见活动,对此您可能甚至不会多加思考。当您想到数据存储时,您自然会考虑一种可以保存当前数据状态的格式。尽管有一些大规模系统(如保险或银行业)可以仔细跟踪和记录任何软件操作,但保存当前数据状态足以满足大多数应用程序和网站的需求。

当前状态存储做法是获取系统状态的快照,并持久保存。数据通常驻留在关系数据库中。这足以执行新的事务,并检索过去事务的结果。在过去十年里,当前状态存储空间不足的情况并不太常见。

最近以来,业务环境一直在飞速变化。跟踪业务和域事件变得越来越有必要。事件源 (ES) 是一种影响存储体系结构以及存储数据的检索和插入方式的模式。ES 不仅用于审核和记录持久域中的业务相关事件,还用于借助较低的抽象级别来保存您的数据,并通过临时工具和模式来创建多个数据投影。

ES 看似是非常智能炫酷的模式,可以记录和审核业务功能,但实际上它是一个全新的存储模型理论,就像是关系模型刚构建时一样。它对新型软件的影响甚至可能大于对 NoSQL 存储的影响。ES 无法替代今天的关系产品和 NoSQL 产品。您可以在关系数据库和 NoSQL 数据存储的基础之上实现 ES。ES 将会改变您对应用程序存储的看法,并会使用事件(而不是状态值)作为数据令牌。

何时使用事件源会有帮助?

超越当前状态存储的一种基本但又常见的方法是跟踪更新历史记录。以一个简单的书店应用程序为例。每本书都有各自的描述属性,且您授予用户编辑权限。当用户输入新的描述时,您应该跟踪旧的描述吗?

每个人的需求都可能各不相同,但在此示例中,跟踪更新十分重要。您将如何实现? 一种方法是存储当前图书状态,并在单独的表中记录所有更新详细信息。每次更新都可以有一条对应的记录。更新记录会包含更新差异,如每个修改过的列的旧值和新值。

您也可以采用其他方法进行实现。“书”表中可以包含标记了给定 ID 的同一本书的多条记录。每条记录均代表按顺序列出的已加盖时间戳的状态(请参阅图 1)。

保留实体历史记录的多条记录
图 1:保留实体历史记录的多条记录

此方案需要使用一个临时 API 来读取当前记录状态。它并不只是存储库中按 ID 选择记录的查询。您必须选取一个具有最新时间戳或最高更新连续编号的记录。此外,与给定的数据实体相关的所有事件的集合形成了流。事件流是 ES 中的热门概念。

每当业务要求您跟踪一系列事件时,ES 都相当有帮助。虽然 ES 看起来可能与日志记录或审核等横切关注点类似,但实际上大不一样。它不会记录事件来配置处理或跟踪异常情况。它只会跟踪业务事件。此外,它也不是横切关注点,而是一个主要应用于存储的体系结构决策。

定义的事件源

简而言之,ES 就是将事件用作主要数据源。由于 ES 并不一定适用于所有应用程序,因此开发者乐而忘忧,数十年来都忽略了它。如果您现在认为 ES 似乎毫无用处,那么主要是因为您尚不需要它。

我要总结一下使用 ES 的必要性,如下所述: 如果域专家需要跟踪软件可以生成的一系列事件,则事件源是一个可行的选择。在其他情况下,事件可能仍可用于表达工作流和连接各业务逻辑。不过,在这种情况下,事件并不是域中的高级别成员,可能不会持久保存。这就是今天的主流方案。

接下来,我们要来探讨当事件是您应用程序的主数据源时,您需要做些什么。ES 影响存储的两个方面:持久性和查询。持久性以 3 项核心操作为特征:插入、更新和删除。在 ES 方案中,插入操作仍几乎与保存当前实体状态的典型系统中的插入操作相同。系统接收请求,并将新事件写入存储。事件中包含唯一标识符(例如 GUID)、类型名称或标识事件类型的代码、时间戳和相关信息。

更新操作由数据实体的同一容器中的另一个插入操作组成。新条目仅反映以下数据相关信息:哪些属性已改变、新值是什么、是否与业务域相关、为什么要更改以及是如何更改的。在执行更新操作后,数据存储会不断演进,如图 2 所示。

指明更新 ID 为 #1 的实体的新记录
图 2:指明更新 ID 为 #1 的实体的新记录

删除操作与更新操作的工作方式几乎相同,区别仅在于,删除操作包含不同的信息,明确指明这是一项删除操作。

在查询方面,如此进行更新会立刻导致一些问题出现。您如何知道是否存在给定记录,或者当前记录状态可能是什么? 这就需要使用一个临时查询层,从概念上选择具有匹配 ID 的所有记录,然后逐个事件分析数据集。

例如,它可以根据已创建事件的内容新建一个数据实体。然后,它会重播所有连续的步骤,并返回流结束时保留的信息。我们将这项技术称为事件重播。通过简单地重播事件来重新生成状态可能会引发一些性能担忧。

以银行账户为例。某客户多年前开立了一个银行账户,从那时起就积累了数百个操作和事件。若要获取当前余额,您必须重播几百个操作,才能重建当前账户状态。这并不总是可行。

对于此情景,解决办法是有的。最重要的是创建快照。快照是在给定的时间保存实体的已知状态的记录。这样一来,就无需重播快照日期之前的事件了。

ES 并不与任何技术或产品绑定,无论是特定的关系数据库,还是 NoSQL 数据存储。ES 确实提高了人们对至少一个特殊软件组件(即事件存储)的需求性。从本质上来讲,事件存储就是事件日志。您可以使用您自己的代码,在您选择的任意数据存储 API 的基础之上创建一个。

事件存储具有两大主要特征。首先,它是一种仅限追加的数据存储。它不支持更新操作,可能会仅有选择性地支持特定类型的删除操作。其次,事件存储必须能够返回与给定密钥相关的事件流。您可以自行创建这一层代码,也可以使用可用的工具和框架。

事件存储选项

您可以使用任何有效的选项来实现事件存储。它通常使用关系数据库或某个版本的 NoSQL 数据存储作为持久引擎。如果您计划使用关系数据库,则可以每种实体类型一个表(每个事件各占一行)。

事件通常有不同的布局。例如,每个事件可能有不同数量的属性要保存,这样一来,您就很难为所有行制定出一个公用的架构。如果可以根据所有可能列的集合生成一个公用架构,且此架构的运行效果是可接受的,那么这就是一个可以轻松实现的选项。

在其他情况下,您可以考虑 SQL Server 2014 的列索引存储功能,此功能可将表配置为在垂直列(而不是水平行)中存储数据。适用于所有版本的 SQL Server 的另一个选项是,将事件属性规范化为一个 JSON 对象,并将它作为字符串存储在一列中。

在 NoSQL 行话中,“文档”是指属性数量可变的对象。一些 NoSQL 产品专用于存储文档。从开发者的角度来看,这再简单不过了。只需创建一个类,在其中填充值,然后按原样进行存储即可。类的类型是关联多个事件的关键信息。如果您使用 NoSQL,那么您就有一个事件对象,只需保存它即可。

正在执行的项目

ES 是相对较新的体系结构方法。有助于在基于事件的数据存储的基础之上编写代码的标准工具仍在不断涌现。虽然您肯定可以自行安排 ES 解决方案,但也可以使用一些临时工具,它们能够帮助您以更结构化的方式处理事件存储。

使用事件感知数据存储的主要优势在于,数据库等工具可保证您只执行读取和追加事件的操作,以确保事件源方法的业务一致性。一个专为存储事件而设计的框架是 NEventStore 项目 (neventstore.org)。此项目可方便您写入和读取回发事件,并不受持久性制约进行操作。保存事件的方法如下:

var store = Wireup.Init()
  .UsingSqlPersistence("connection")
  .InitializeStorageEngine()
  .UsingJsonSerialization()
  .Build();
var stream = store.CreateStream(aggregateId);
stream.Add(new EventMessage { Body = eventToSave });
stream.CommitChanges(aggregateId);

若要读取回发事件,请通过已提交的事件的集合打开流并循环。

事件存储 (geteventstore.com) 的工作原理也是为事件流提供适用于普通 HTTP 和 .NET 的 API。在 ES 行话中,“聚合”等同于存储中的流。您可以对事件流执行 3 项基本操作:写入事件;读取上一个事件、特定事件和多个事件中的一个;以及订阅以获得更新。

订阅分为以下 3 种类型。第一种是易失订阅,即将事件写入给定的流会调用回叫函数(每次都被调用)。第二种是弥补订阅,即对于存储中的每个事件(从给定事件开始)以及之后添加的任何新事件,您都会收到通知。最后一种是持久订阅,适用于多个使用者正在等待处理事件的情形。订阅可保证事件至少传递给使用者一次,但实际可能按照无法预知的顺序传递多次。

总结

事件源使用事件作为应用程序数据源。您无需构建应用程序来保存实体的上次已知状态,但需要构建相关业务事件的列表。事件数据源在较低的抽象级别存储数据。您需要从那里将投影应用到事务和查询所需的实际实体状态。投影是指重播事件和执行一些任务的过程。最明显的投影是构建当前状态;但您可以拥有任何数量或类型的非事件投影。


Dino Esposito是《Microsoft .NET: Architecting Applications for the Enterprise》(Microsoft Press,2014 年)和《Programming ASP.NET MVC 5》(Microsoft Press,2014 年)的合著者。作为 JetBrains 的 Microsoft .NET Framework 和 Android 平台的技术推广人员,Esposito 经常在全球行业活动中发表演讲,并在 software2cents.wordpress.com 上以及 twitter.com/despos 上的推文中分享他对于软件的愿景。

衷心感谢以下技术专家对本文的审阅: Jon Arne Saeteras