使用RDBMS作为事件源存储


119

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

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

例如,假设某人拥有一个“产品”实体,那么对该产品的更改可以采用以下形式:价格,成本和说明。我对是否愿意感到困惑:

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

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

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


2
最简单的形式:[事件] {AggregateId,AggregateVersion,EventPayload}。不需要聚合类型,但是您可以选择存储它。不需要事件类型,但是您可以选择存储它。这是一长串的事情,其他都是优化。
伊夫·雷恩豪特

7
绝对远离#1和#2。将所有内容序列化为blob并以这种方式存储。
乔纳森·奥利弗

Answers:


109

事件存储区不需要了解事件的特定字段或属性。否则,对模型的每次修改都将导致必须迁移数据库(就像在良好的老式基于状态的持久性中一样)。因此,我完全不会推荐选项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]

9
好答案!我一直在阅读的有关使用EventSourcing的主要论据之一是查询历史记录的能力。当所有有趣的数据都序列化为XML或JSON时,我将如何制作一个有效查询的报告工具?是否有有趣的文章正在寻找基于表的解决方案?
Marijn Huizendveld '02

11
@MarijnHuizendveld,您可能不想查询事件存储本身。最常见的解决方案是连接几个事件处理程序,以将事件投影到报告数据库或BI数据库中。针对这些处理程序重播事件历史记录。
丹尼斯·特劳布

1
@Denis Traub感谢您的回答。为什么不针对事件存储本身进行查询?恐怕如果每次我们提出一个新的BI案例时都必须重播全部历史记录,它将变得非常混乱/强烈。
Marijn Huizendveld '02

1
我想在某个时候您应该在事件存储区之外还拥有一些表,以便以最新状态存储模型中的数据?然后将模型分为读取模型和写入模型。写模型与事件存储冲突,事件存储武术将更新为读模型。读取模型包含代表系统中实体的表-因此您可以使用读取模型进行报告和查看。我一定有误会。
theBoringCoder

10
@theBoringCoder听起来好像您对事件源和CQRS感到困惑或至少混在了脑海中。它们经常在一起发现,但它们不是同一回事。CQRS使您分离读写模型,而Event Sourcing使您将事件流用作应用程序中唯一的事实来源。
布莱恩·安德森

7

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

我想这里的要点是它们都符合相同的委托/合同。他们都将信息存储在单个位置/容器/表中,它们使用元数据从另一个事件中识别一个事件,并“按原样”存储整个事件-在某些情况下是序列化的,并支持技术。因此,取决于您选择文档数据库,关系数据库乃至平面文件,都有几种不同的方式来达到事件存储的相同意图(如果您随时改变主意并发现需要迁移或支持,这将非常有用。多种存储技术)。

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

首先,出于多种原因,我们发现(甚至使用唯一的UUID / GUID而不是整数),由于战略原因出现了顺序ID,因此仅ID不足以作为键,因此我们将主ID键列与data /对象类型来创建真正的(就您的应用程序而言)唯一键。我知道有人说您不需要存储它,但这取决于您是未开发的环境还是必须与现有系统共存。

出于可维护性的原因,我们只使用一个容器/表/集合,但是每个实体/对象都使用单独的表。我们在实践中发现,这意味着该应用程序需要“创建”权限(通常来说这不是一个好主意……通常,总是存在异常/排除),或者每次出现或部署新的实体/对象时,新的需要进行存储的容器/表格/收集。我们发现这对于本地开发而言非常缓慢,而对于生产部署则存在问题。您可能没有,但这是我们的真实经验。

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

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

其他生产中的元数据在基于生产的调查中很有用,时间戳使我们可以洞悉事件发生与发生的顺序。这为我们提供了一个特别重要的事件驱动系统的帮助,该系统引发了大量事件,为我们提供了有关诸如网络之类的事物的性能以及整个网络中系统分布的信息。


太好了,谢谢。碰巧的是,自从写了这个问题很久以来,我就在github的Inforigami.Regalo库中建立了一些自己的库。RavenDB,SQL Server和EventStore实现。很想笑一个基于文件的文件。:)
尼尔·巴恩威尔

1
干杯。我主要为最近才遇到此问题并分享一些经验教训而不只是结果的其他人添加了答案。
cdmdotnet

3

好吧,您可能想看看Datomic。

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

在这里写了详细的答案

您可以在此处观看Stuart Halloway的讲解Datomic设计的演讲

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


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

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


我认为BatchId可能与CorrelationId和CausationId有关。用于找出导致事件的原因,并在需要时将它们串在一起。
丹尼尔公园

它可能是。不管是这样,提供一种自定义它的方法(例如,将其设置为请求的id)是有意义的,但是框架没有这样做。
Fabio Marreco

1

可能的提示是在设计之后加上“缓慢更改尺寸”(类型= 2)应该可以帮助您解决:

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

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


1

我认为这将是一个较晚的答案,但我想指出的是,如果您的吞吐量要求不高,则可以将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的插入速度很慢,尤其是在插入总是附加时。

我希望该代码可以为您提供示例,除了已经为该问题提供的非常好的理论答案之外。


谢谢。我已经建立了一个基于SQL的实现很久了。我不确定为什么RDBMS插入速度慢,除非您对某个地方的集群键做出了无效的选择。仅追加应该没问题。
Neil Barnwell
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.