131

如果我使用 RDBMS(例如 SQL Server)来存储事件溯源数据,架构会是什么样子?

我已经看到了一些抽象意义上的变体,但没有具体的。

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

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

特别是我担心上面的选项2。极端情况下,产品表几乎是每个属性一个表,在哪里加载给定产品的应用程序状态将需要从每个产品事件表中加载该产品的所有事件。这种桌子爆炸对我来说是错误的。

我确信“这取决于”,虽然没有单一的“正确答案”,但我试图了解什么是可以接受的,什么是完全不可接受的。我也知道 NoSQL 在这里可以提供帮助,其中可以针对聚合根存储事件,这意味着只需向数据库发出一个请求即可获取从中重建对象的事件,但我们没有使用 NoSQL 数据库时刻,所以我正在寻找替代方案。

4

6 回答 6

119

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

以下是Ncqrs中使用的架构。如您所见,“事件”表将相关数据存储为 CLOB(即 JSON 或 XML)。这对应于您的选项 3(只是没有“ProductEvents”表,因为您只需要一个通用的“Events”表。在 Ncqrs 中,映射到您的聚合根是通过“EventSources”表发生的,其中每个 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 持久性机制基本上由一个名为“Commits”的表和一个 BLOB 字段“Payload”组成。这与 Ncqrs 中的几乎相同,只是它将事件的属性以二进制格式序列化(例如,添加了加密支持)。

Greg Young 推荐了一种类似的方法,正如Greg 的网站上广泛记录的那样。

他的原型“事件”表的架构如下:

Table Events
    AggregateId [Guid],
    Data [Blob],
    SequenceNumber [Long],
    Version [Int]
于 2011-08-15T13:02:12.060 回答
10

GitHub 项目CQRS.NET提供了一些具体示例,说明如何使用几种不同的技术实现 EventStore。在撰写本文时,有一个使用 Linq2SQL的 SQL 实现和一个与之配套的SQL 模式,一个用于MongoDB,一个用于DocumentDB(如果您在 Azure 中,则为 CosmosDB)和一个使用EventStore(如上所述)。Azure 中还有更多类似于平面文件存储的表存储和 Blob 存储。

我想这里的要点是它们都符合相同的委托人/合同。它们都将信息存储在一个地方/容器/表中,它们使用元数据来从另一个事件中识别一个事件,并“只是”按原样存储整个事件——在某些情况下,在支持技术的情况下是序列化的。因此,根据您选择的是文档数据库、关系数据库还是平面文件,有几种不同的方法可以达到事件存储的相同意图(如果您在任何时候改变主意并发现需要迁移或支持,这很有用一种以上的存储技术)。

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

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

出于可维护性的原因,我们坚持使用单个容器/表/集合,但我们确实为每个实体/对象使用了一个单独的表。我们在实践中发现,这意味着要么应用程序需要“CREATE”权限(一般来说这不是一个好主意……通常,总会有例外/排除),或者每次新实体/对象出现或部署时,新的需要制作存储容器/桌子/收藏品。我们发现这对于本地开发来说非常缓慢并且对于生产部署来说是有问题的。你可能不会,但那是我们的真实体验。

另一件要记住的事情是,要求动作 X 发生可能会导致许多不同的事件发生,因此知道由命令/事件/有用的东西生成的所有事件。它们还可能跨越不同的对象类型,例如在购物车中推送“购买”可能会触发帐户和仓储事件。一个消费应用程序可能想知道所有这些,所以我们添加了一个 CorrelationId。这意味着消费者可以请求所有因他们的请求而引发的事件。您将在schema中看到这一点。

特别是对于 SQL,我们发现如果没有充分使用索引和分区,性能确实会成为瓶颈。请记住,如果您使用快照,则需要以相反的顺序流式传输事件。我们尝试了一些不同的索引,发现在实践中,需要一些额外的索引来调试生产中的实际应用程序。您将再次在schema中看到这一点。

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

于 2017-08-06T23:53:02.910 回答
3

好吧,您可能想看看 Datomic。

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

我在这里写了详细的答案

你可以在这里观看 Stuart Halloway 的演讲,解释 Datomic 的设计

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

于 2013-07-20T08:37:52.277 回答
2

我认为随着域模型的发展,解决方案(1 和 2)会很快成为一个问题。新字段被创建,一些改变了含义,一些可能不再使用。最终,您的表将有数十个可为空的字段,并且加载事件将变得一团糟。

另外,请记住,事件存储应仅用于写入,您仅查询它以加载事件,而不是聚合的属性。它们是独立的东西(这是 CQRS 的本质)。

解决方案 3 人们通常会做的事情,有很多方法可以实现。

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

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:简单的全局标识,可用于在创建投影(读取模型)时排序或标识丢失的事件。
  • BatchId:原子插入的事件组的标识(TBH,不知道为什么这会有用)
  • AggregateId:聚合的标识
  • 数据:序列化事件
  • 元数据:来自事件的其他有用信息(例如,用于反序列化的事件类型、时间戳、命令的发起者 ID 等)
  • AggregateSequenceNumber:同一聚合中的序列号(如果您不能让写入发生乱序,这很有用,因此您可以使用此字段进行乐观并发)

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

于 2019-06-28T15:00:51.777 回答
1

可能的提示是设计后跟“缓慢变化的维度”(type=2)应该可以帮助您涵盖:

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

左折叠功能也应该可以实现,但您需要考虑未来的查询复杂性。

于 2015-03-13T00:06:08.417 回答
1

我认为这将是一个较晚的答案,但我想指出,如果您的吞吐量要求不高,则完全可以使用 RDBMS 作为事件源存储。我将向您展示我构建的事件溯源分类账的示例以进行说明。

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

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

除了已经为这个问题提供的非常好的理论答案之外,我希望代码可以帮助您说明问题。

于 2019-09-23T03:23:48.110 回答