使用RDBMS作为事件源存储

时间:2011-08-15 12:44:39

标签: cqrs event-sourcing

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

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

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

  1. 拥有一个“ProductEvent”表,其中包含产品的所有字段,其中每个更改表示该表中的新记录,以及“适当的人员,内容,地点,原因,时间和方式”。当成本,价格或描述发生变化时,会添加一个全新的行来代表产品。
  2. 将产品成本,价格和描述存储在使用外键关系连接到Product表的单独表中。当发生对这些属性的更改时,请根据需要使用WWWWWH写入新行。
  3. 在“ProductEvent”表中存储WWWWWH以及表示事件的序列化对象,这意味着必须在我的应用程序代码中加载,反序列化并重新播放事件本身,以便重新构建应用程序状态。给定的产品。
  4. 特别是我担心上面的选项2。极端情况下,产品表几乎是每个表一个表,在这里加载给定产品的应用程序状态需要从每个产品事件表加载该产品的所有事件。这桌爆炸对我来说有点不对劲。

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

6 个答案:

答案 0 :(得分:97)

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

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

Greg Young建议采用与extensively documented on Greg's website类似的方法。

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

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

答案 1 :(得分:6)

GitHub项目CQRS.NET提供了一些具体示例,说明如何使用几种不同的技术来执行EventStore。在撰写本文时,SQL using Linq2SQLSQL schema有一个实现,MongoDBDocumentDBEventStore有一个(CosmosDB,如果你& #39;在Azure中)和一个使用schema(如上所述)。 Azure中的更多内容,如表存储和Blob存储,与平面文件存储非常相似。

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

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

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

出于可维护性原因,我们坚持使用单个容器/表/集合,但我们确实为每个实体/对象使用了一个单独的表。我们在实践中发现,这意味着需要应用程序和#34; CREATE"权限(通常说不是一个好主意......通常,总是有例外/排除)或每次新的实体/对象出现或部署时,需要新的存储容器/表/集合制作。我们发现这对于本地开发来说非常缓慢,并且对于生产部署来说也存在问题。你可能没有,但这是我们的真实经历。

要记住的另一件事是,要求操作X发生可能会导致发生许多不同的事件,从而了解命令/事件/生成的所有事件。它们也可以跨越不同的对象类型,例如推动"购买"在购物车中可以触发帐户和仓储事件来触发。消费应用程序可能想知道所有这些,因此我们添加了CorrelationId。这意味着消费者可以询问因其请求而提出的所有事件。您会在schema

中看到这一点

特别是对于SQL,我们发现如果索引和分区没有得到充分利用,性能确实成了瓶颈。如果使用快照,请记住,事件需要以相反的顺序进行流式处理。我们尝试了一些不同的索引,发现在实践中,需要一些额外的索引来调试生产中的实际应用程序。您再次在https://jsfiddle.net/z9pq1026/

中看到了这一点

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

答案 2 :(得分:3)

嗯,你可能想看看Datomic。

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

我写了详细的答案here

您可以观看Stuart Halloway的演讲,解释Datomic here

的设计

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

答案 3 :(得分:1)

可能的提示是设计后跟“慢慢改变尺寸”(type = 2)应该可以帮助你覆盖:

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

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

答案 4 :(得分: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的插入速度很慢,尤其是在插入总是附加时。

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

答案 5 :(得分:0)

我认为随着域模型的发展,解决方案(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原则,并为您的用例创建最少的必填字段。