使用 RDBMS 作为事件源存储

如果我使用 RDBMS (例如 SQLServer)来存储事件源数据,那么模式是什么样子的?

我见过一些抽象意义上的变体,但没有具体的。

例如,假设有一个“产品”实体,对该产品的更改可能以下列形式出现: 价格、成本和描述。我不知道我是否会:

  1. 有一个“ ProductEvent”表,其中包含产品的所有字段,其中每个更改都意味着该表中的一个新记录,以及“ who,what,where,why,when and how”(WWWWWH)。当成本、价格或描述发生变化时,添加一个全新的行来表示产品。
  2. 将产品成本、价格和描述存储在与 Product 表连接的具有外键关系的单独表中。当这些属性发生更改时,使用 WWWWWH 适当地写入新行。
  3. 在“ ProductEvent”表中存储 WWWWWH,加上一个表示事件的序列化对象,这意味着事件本身必须在我的应用程序代码中加载、反序列化并重新播放,以便为给定的 Product 重新构建应用程序状态。

我尤其担心选项2。在极端情况下,产品表几乎是每个属性一个表,在这里为给定的产品加载 Application State 将需要从每个产品事件表加载该产品的所有事件。我觉得这桌子爆炸的味道不对。

我确信“这取决于”,虽然没有单一的“正确答案”,我试图得到一个什么是可以接受的,什么是完全不可以接受的感觉。我也知道 NoSQL 在这方面有所帮助,事件可以根据聚合根存储,这意味着只需要向数据库发出一个请求,就可以获得用于重新构建对象的事件,但我们目前没有使用 NoSQL 数据库,所以我正在寻找替代方案。

45140 次浏览

事件存储不应该需要了解事件的特定字段或属性。否则,模型的每次修改都将导致必须迁移数据库(就像在传统的基于状态的持久化中一样)。因此,我根本不推荐选项1和选项2。

下面是 Ncqrs中使用的模式。如您所见,表“ Events”将相关数据存储为 CLOB (即 JSON 或 XML)。这对应于选项3(只是不存在“ ProductEvents”表,因为您只需要一个通用的“ Events”表。在 Ncqr 中,到聚合根的映射通过“ EventSource”表进行,其中每个 EventSource 对应一个实际的聚合根。)

Table Events:
Id [uniqueidentifier] NOT NULL,
TimeStamp [datetime] NOT NULL,


Name [varchar](max) NOT NULL,
Version [varchar](max) NOT NULL,


EventSourceId [uniqueidentifier] NOT NULL,
Sequence [bigint],


Data [nvarchar](max) NOT NULL


Table EventSources:
Id [uniqueidentifier] NOT NULL,
Type [nvarchar](255) NOT NULL,
Version [int] NOT NULL

Jonathan Oliver 的活动商店实现的 SQL 持久化机制基本上由一个名为“ Commit”的表和一个 BLOB 字段“ Payload”组成。这与 Ncqrs 的情况大致相同,只是它以二进制格式序列化事件的属性(例如,增加了加密支持)。

Greg Young 建议使用类似的方法,如 在格雷格的网站上有大量记录

他的原型“ Events”表的模式如下:

Table Events
AggregateId [Guid],
Data [Blob],
SequenceNumber [Long],
Version [Int]

你可能会想看看达原子。

Datom 是一个灵活的数据库 基于时间的事实,支持查询和连接,具有弹性可伸缩性和 ACID 事务。

我写了一个详细的答案 给你

你可以观看 Stuart Halloway 的演讲,解释数字原子 给你的设计

由于 Datomic 及时存储事实,因此您可以将其用于事件源用例等等。

可能的提示是设计后面的“缓慢变化的尺寸”(类型 = 2)应该帮助你覆盖:

  • 事件发生的顺序(通过代理键)
  • 每个状态的持久性(从有效到有效)

左折叠函数也可以实现,但是您需要考虑将来的查询复杂性。

GitHub 项目 CQRS.NET提供了一些具体的示例,说明如何使用几种不同的技术实现 EventStores。在写这篇文章的时候,有一个 使用 Linq2SQL 的 SQL和一个 SQL 架构的实现,有一个用于 MongoDB,一个用于 文档数据库(如果你在 Azure 中使用 CosmosDB) ,还有一个用于 EventStore(如上所述)。Azure 中有更多类似于 Table Storage 和 Blob 存储的东西,它们非常类似于平面文件存储。

我想这里的主要观点是他们都遵守同样的本金/合同。它们都将信息存储在一个单独的地方/容器/表中,它们使用元数据来识别一个事件和另一个事件,并且“仅仅”存储整个事件——在某些情况下是序列化的,在支持技术中是如此。因此,如果你选择一个文档数据库、关系数据库甚至是平面文件,有几种不同的方法可以达到同一个事件存储的目的(如果你在任何时候改变主意,发现需要迁移或支持多种存储技术,这很有用)。

作为这个项目的开发人员,我可以分享一些关于我们所做选择的见解。

首先,我们发现(即使使用唯一的 UUID/GUID 而不是整数)出于很多原因,连续的 ID 出现是出于战略原因,因此只有一个 ID 对于一个键来说不够唯一,所以我们合并了我们的主 ID 键列和数据/对象类型来创建一个真正的(在应用程序的意义上)唯一键。我知道有些人说你不需要存储它,但这将取决于你是新手还是必须与现有系统共存。

出于可维护性的原因,我们坚持使用单个容器/表/集合,但我们确实对每个实体/对象使用了单独的表。我们在实践中发现,这意味着要么应用程序需要“ CREATE”权限(一般来说这不是一个好主意... ... 一般来说,总会有例外/排除) ,要么每当一个新的实体/对象出现或部署时,都需要创建新的存储容器/表/集合。我们发现,这对于本地开发来说非常缓慢,对于生产部署来说也存在问题。你可能不知道,但那是我们在现实世界的经历。

另一件需要记住的事情是,要求操作 X 发生可能会导致许多不同的事件发生,从而知道由命令/事件/任何有用的东西生成的所有事件。它们也可能跨不同的对象类型,例如在购物车中按下“购买”键可能会触发帐户和仓库事件。消费应用程序可能希望了解所有这些信息,因此我们添加了一个相关 ID。这意味着使用者可以请求由于其请求而引发的所有事件。你会在 模式中看到的。

特别是对于 SQL,我们发现如果索引和分区没有得到充分使用,那么性能真的会成为一个瓶颈。请记住,如果使用快照,则需要以相反的顺序对事件进行流处理。我们尝试了几种不同的索引,发现在实际操作中,需要一些额外的索引来调试生产中的实际应用程序。你会在 模式中再次看到。

其他生产中的元数据在基于生产的调查期间是有用的,时间戳让我们了解了事件持久化与提出的顺序。这给了我们一些特别重事件驱动系统的帮助,它引发了大量的事件,给了我们关于网络性能和系统在整个网络中的分布的信息。

我认为解决方案(1 & 2)可以成为一个问题很快,因为您的领域模型的发展。创建了新字段,一些字段改变了含义,一些字段可能不再使用。最终,您的表将有几十个可为空的字段,并且加载事件将是混乱的。

另外,请记住事件存储应该只用于写操作,您只需要查询它来加载事件,而不是聚合的属性。它们是独立的事物(这是 CQRS 的本质)。

解决方案3人们通常做什么,有许多方法来完成。

例如,与 SQLServer 一起使用时,EventFlow CQRS创建一个具有此架构的表:

CREATE TABLE [dbo].[EventFlow](
[GlobalSequenceNumber] [bigint] IDENTITY(1,1) NOT NULL,
[BatchId] [uniqueidentifier] NOT NULL,
[AggregateId] [nvarchar](255) NOT NULL,
[AggregateName] [nvarchar](255) NOT NULL,
[Data] [nvarchar](max) NOT NULL,
[Metadata] [nvarchar](max) NOT NULL,
[AggregateSequenceNumber] [int] NOT NULL,
CONSTRAINT [PK_EventFlow] PRIMARY KEY CLUSTERED
(
[GlobalSequenceNumber] ASC
)

地点:

  • GlobalSequenceNumber : 简单的全局标识,可用于在创建投影(readmodel)时排序或标识缺少的事件。
  • BatchId : 标识原子插入的事件组(TBH,不知道为什么这样做有用)
  • AgregateId : 聚合的识别
  • Data : 序列化事件
  • 元数据 : 来自事件的其他有用信息(例如用于反序列化的事件类型、时间戳、来自命令的原创者 ID 等)
  • AgregateSequenceNumber : 同一个聚合中的序列号(如果不能让写操作顺序错误,这个字段非常有用,因此可以使用这个字段进行乐观并发操作)

但是,如果您从头开始创建,我建议遵循 YAGNI 原则,并使用用例所需的最小字段进行创建。

我认为这是一个迟到的回答,但是我想指出的是,如果吞吐量需求不高,使用 RDBMS 作为事件源存储是完全可能的。我只是想向您展示一个我构建来说明的事件源分类账的示例。

Https://github.com/andrewkkchan/client-ledger-service 上面是一个事件源分类账 Web 服务。 Https://github.com/andrewkkchan/client-ledger-core-db 上面我使用 RDBMS 来计算状态,因此您可以享受 RDBMS 带来的所有优势,比如事务支持。 Https://github.com/andrewkkchan/client-ledger-core-memory 我还要在内存中处理另一个消费者来处理突发事件。

有人可能会说,上面的实际事件存储仍然存在于 Kafka ——因为 RDBMS 插入速度很慢,尤其是当插入总是在附加时。

我希望这段代码除了已经为这个问题提供了非常好的理论上的答案之外,还能给您提供一个说明。