使用 Cassandra 作为事件存储

Posted

技术标签:

【中文标题】使用 Cassandra 作为事件存储【英文标题】:Using Cassandra as an event store 【发布时间】:2013-10-11 15:20:18 【问题描述】:

我想尝试在事件溯源应用程序中。我对活动商店的要求非常简单。事件“模式”是这样的:

id:聚合根实体的id data:序列化的事件数据(例如 JSON) 时间戳:事件发生的时间 sequence_number:事件的唯一版本

我对 Cassandra 完全陌生,所以请原谅我对我将要写的内容一无所知。我只有两个想要对这些数据运行的查询。

    给我给定聚合根 ID 的所有事件 如果序列号为 > x,请提供给定聚合根的所有事件

我的想法是在 CQL 中创建一个 Cassandra 表,如下所示:

CREATE TABLE events (
  id uuid,
  seq_num int,
  data text,
  timestamp timestamp,
  PRIMARY KEY  (id, seq_num) );

这似乎是对问题建模的明智方法吗?而且,重要的是,使用复合主键是否可以让我有效地执行我指定的查询?请记住,在给定的用例中,同一聚合根 ID 可能存在大量事件(具有不同的 seq_num)。

我特别担心的是第二个查询在某种程度上会效率低下(我在这里考虑二级索引......)

【问题讨论】:

一年过去了,我很想知道您使用 cassandra 的事件溯源项目进展如何。 您还希望所有事件都按时间顺序重建查询模型,这似乎是合乎逻辑的。为此,cassandra 似乎很难处理。 最后我使用了 Akka Persistence 和 Cassandra 日志插件,从而将模式决策委托给插件,而不是设计我自己的模式。 Akka Persistence 作为一种使用 Actor 模型实现 DDD 的方法非常有效。通过遵循每个持久参与者的单个聚合根方法(整个集群中的单个聚合根),它确保事件按时间顺序写入。我建议查找 Akka Cluster Sharding 以获取确保整个集群中每个聚合根的唯一参与者的详细信息。 【参考方案1】:

您的设计似乎很好地以“cassandra 术语”建模。 “复合键”表确实支持您需要的查询,您将拥有如下内容:

查询1:select * from events where id = 'id_event'; 查询2:select * from events where id = 'id_event' and seq_num > NUMBER;

我认为第二个查询不会效率低下,但是它可能会返回很多元素...如果是这种情况,您可以设置要返回的事件的“限制”。如果可能的话,您可以使用limit 关键字。

使用复合键似乎很适合您的特定要求。使用“二级索引”似乎并没有带来太多好处……除非我错过了您的设计/要求中的某些内容。

HTH。

【讨论】:

感谢您的建议。我只是提出二级索引,因为我不确定它是否与复合键有关。【参考方案2】:

您所拥有的一切都很好,但特定聚合的许多事件除外。您可以做的一件事是创建一个静态列来保存“next”和“max_sequence”。想法是静态列将保存此分区的当前最大序列,以及下一个分区的“人工 id”。例如,您可以在每个分区存储 100 或 1000 个事件。您基本上所做的是将聚合的事件存储到多个分区中。这将意味着查询和存储的额外开销,但同时可以防止无限增长。您甚至可以为聚合创建分区查找。真的取决于您的用例以及您希望它有多“聪明”。

【讨论】:

【参考方案3】:

我一直在将 Cassandra 用于非常相似的场景(每行 100k+ 列),并以与您的模型接近的模型结束。我也同意 emgsilva 的观点,二级索引可能不会带来太多。

事实证明,对于我们的事件存储的良好性能而言,有三件事很重要:使用复合列,确保列的排序顺序良好(Cassandra 将数据按行按列排序),以及使用紧凑存储如果可能的话。

请注意,紧凑存储意味着您只能拥有一个值列。因此,您需要使所有其他列成为键的一部分。

对你来说,架构是:

CREATE TABLE events (
    id uuid,
    seq_num int,
    timestamp timestamp,
    data text,
    PRIMARY KEY  (id, seq_num, timestamp))
    WITH COMPACT STORAGE;

【讨论】:

【参考方案4】:

您的分区键过于精细,您应该创建一个复合分区键或更改它以获得更好的时间序列建模性能。比如

CREATE TABLE events (
    event_date int,
    id timeuuid,
    seq_num int,
    data text,
    PRIMARY KEY  (event_date, id) );

这样,您的 id 将成为一个集群列,以保证事件的唯一性,并且您的分区键(即 20160922)可以每天对所有事件进行分组。您也可以将其更改为月份。避免使用 uuid 使用 timeuuid 代替,它已经存储了时间戳信息。

【讨论】:

虽然这是一个简单的想法,但这很危险,因为每天只有一个节点,并且持有副本的节点将处于负载之下。此外,如果某一天有很多事件(至少一个峰值),这将失败。 这种方法没有失败,取决于应用程序上下文它是否可以实现良好的性能,如果需要,这个想法可以延长到一天的小时数,以避免一天中的潜在峰值,并且所有数据都是均匀分布在环上,没有单点故障,负载由驱动通过gossip协议控制,其他Cassandra微调资源可用于提高性能,不在本线程范围内。【参考方案5】:

该设计似乎与 Cassandra 存储数据的方式一致,这是您主键的第一部分,即您的“id”将用于在单独的节点/v 节点上分区数据(取决于您的集群如何已配置),这将使 Cassandra 非常容易为您的第一个查询获取数据,因为它只需要触及一个分区,现在根据您的密钥的第二部分将是一个集群密钥,即将指定数据的方式在该分区内排序,这就是您的第二个查询的全部内容。请记住,只要您的所有数据都以这样一种方式设计,即对表的每个查询只涉及一个分区,那么您就可以开始了。此外,如果您担心第二个查询会返回大量数据,您始终可以选择 Cassandra 固有地为范围查询提供的分页。

【讨论】:

【参考方案6】:

我不同意您将聚合根保存在 eventstore 上的设计。您应该保存 domainevent 以获得灵活性。 我解释说 eventdomain 是最细粒度的数据,它可以改变 application.aggregateroot 的状态,它与 eventstore 不匹配,它用于数据交换或 boundedcontext。 当您使用域事件时,您甚至可以使用 plolygot 建模来重建数据甚至聚合根。您可以根据客户和约束的需要来管理模型。因此,您可以为域对象之间的链接建模图形,然后使用 neo4j,此外还可以建模聚合模型和您使用文档数据库。我的意思是您有机会更改模型并使用方便的持久性引擎。它是 polygot 数据和 polygot 持久性之间的区别。 在你的策略中,我理解两种方式: 如果您需要事件外包,您可以在 domainevent 和 cassandra 数据库上建模。 如果您需要聚合根数据或模型并且没有事件溯源,则使用文档化数据库,您可以检索这两个查询。

您应该消除对领域驱动设计的困惑。

【讨论】:

回复这个有点晚了..我认为你还没有真正正确地阅读原始帖子或任何回复。感谢您建议我消除我对 DDD 的困惑,尽管我认为您会发现您是在这种情况下感到困惑的人。很明显,讨论是关于存储域事件,然后可以重放这些事件以重建聚合根

以上是关于使用 Cassandra 作为事件存储的主要内容,如果未能解决你的问题,请参考以下文章

EMR LinkageError 上的 Spark + Cassandra

一些Cassandra+YCSB异常

国内哪些互联网公司使用了 Cassandra 数据库?

Cassandra 中的手动压缩

使用 Cassandra 存储不可变数据?

Cassandra 是不是支持聚合功能或 Map Reduce 等任何其他功能?