CQRS的活动巴士

时间:2016-06-10 09:15:14

标签: c# cqrs

我用CQRS写了一个项目。我想从域事件异步构建我的读模型。当我阅读Greg Young和其他关于CQRS的话题时,我看到只是使用服务总线。但对我来说,服务总线是消息传输的技术层,在这种情况下,处理顺序并不重要。

域事件是具有特殊性的消息:处理顺序是重要的。

那么,我是否使用具有附加(复杂)层的服务总线(NServiceBus,Mass transit ...)来确保事件处理顺序?或者特定的"事件总线"具有这种特殊性已经存在?

修改

我更准确地解释了我的情况:

我用CQRS做了第一次申请。在这个应用程序中,我使用关系数据库作为持久层而不是事件源。该数据库用于写入侧和读取侧。这是一个选择。此应用程序在抽象总线上发布域事件,但没有实现。

我需要使用第一个应用程序中的读取模型构建第二个应用程序。

在这种情况下,如果我使用了一个事件存储,我使用和事件存储为我的第二个应用程序构建投影是一件坏事。如果持久层发生更改,则其他应用程序(侦听器)会受到影响。

编辑2:解决方案?

解决方案可能是使用情侣事件存储/服务总线来构建事件总线。因此,持久性无知被保留,事件总线作为构建投影和监听事件的工具。

编辑3:GetEventStore

我看看GetEventStore,它提供了一个订阅机制。 解决方案可能是使用GetEventStore作为事件总线!

因此,如果我使用GetEventStore作为事件总线,它意味着我使用事件采购?或者事件采购只是持久层的概念?或者在我的第一个应用程序中,我已使用事件源,因为我使用聚合中的域事件来构建关系数据库中的状态?

所以新问题是什么是事件采购?将事件直接存储在事件存储中或使用域事件来构建状态?

3 个答案:

答案 0 :(得分:3)

这里有几点需要考虑。

如果您使用Event Sourcing,则可能需要使用GES(geteventstore.com)。在这种情况下,用于构建读取模型的事件不通过任何消息总线,而是使用订阅从事件存储中检索。在这种情况下,您总能获得正确的订单。

如果您在“普通”数据库中只有两个单独的模型,这是一个文档或关系数据库,您可能应该使用一些消息总线。这里也有一些事情:

  • 请记住,更新写入端并将事件发布到消息总线应该在事务中完成。否则,您的阅读模型可能会出现不一致的风险

  • NServiceBus或MassTransit不作为服务总线抽象,与消息排序有关。事实上,你应该选择正确的运输方式,它将完成这项工作。例如,AMQP为消息排序和RabbitMQ delivers设置了一些要求。当然,使用NSB或MT会让你的生活变得更加容易,但这是完全不同的事情

  • 通过将聚合版本保持为双方的字段,您始终可以将您的写入和读取方保留在同一版本上。在这种情况下,你至少可以知道你是否得到一个有差距的事件并决定做什么 - 等待丢失的事件被交付或者只是停止处理和崩溃以允许手动干预

实践表明,所有现代经纪人都支持AMQP并遵循标准。你可能有重复,但这样做不那么危险。

你可以选择使用ZMQ,这样你的延迟几乎为零,所以除非你的数量非常高,否则无序传递信息的变化将是微不足道的。

答案 1 :(得分:1)

  

所以新问题是什么是事件采购?将事件直接存储在事件存储中或使用域事件来构建状态?

"事件来源"是在域事件中捕获所有域模型更改的属性。

  

域事件是具有特殊性的消息:处理顺序是重要的。

是的,这很难理解;如果您正在处理来自任何的事件,您需要阅读历史记录(有序的事件序列),而不仅仅是事件本身。

  

我认为使用事件存储(在选择事件源的情况下)来对读模型进行投影意味着与持久层的不良依赖关系。

完全没有;从阅读模型的角度来看,只有一个返回历史的存储库。实际的实现细节纯粹是一个持久性问题。

  

那么,我是否使用带有附加(复杂)层的服务总线(NServiceBus,Mass transit ...)来确保事件处理顺序

几乎 - 您使用任何您喜欢的事件发布者,使用简单图层来确保事件处理顺序。

  

我想从域事件中异步构建我的读模型。

因此,您的订阅者会查看每个事件,识别它关注的事件,并跟踪哪些历史记录已过时。在重建模型时,您需要重新加载已更改的历史记录(如果您一直在跟踪,则会获取这些历史记录的更新)。

如果您的事件中包含编码的版本信息,您可以做得更好一些,因为您可以跟踪您在流中的位置;并丢弃任何" old"知道你已经处理过的事件。

也就是说,想象您的订阅者更新地图的内存副本,该副本会跟踪正在观看的每个流/历史/聚合中的最新事件

{ user.7 : 14
, order.14 : 27
, order.12 : 4
}

在重建读取模型的时候,您复制该地图,然后将其交给将构建模型的流程。该进程有自己的映射,描述用于重建先前版本的流的版本。

{ user.7 : 14
, order.14 : 22
, order.12 : 5
}

在这里,该过程可以看到先前的预测在user.7上是最新的,在order.14上是后面的,在order.12上是前面的(可能是订阅者错过了一个事件,或者它没有&t; t到了,或者某事。)

由于我们知道状态是状态,我们重建了读模型;你可以从头开始重建所有东西,或者你可以注意到只有order.14已经过时,并从那里重建。获取所需的历史记录,创建读取模型,保存用于创建该模型的流的版本,然后就完成了。

  

如果第一个应用程序更改了其持久层,则其他应用程序会受到影响,不是吗?

不一定 - 如果你想要我的数据,那么你跟我说话 - 而不是我的持久层。你应该回顾一下Udi Dahan对于[特定业务能力] [{3]的technical authority服务所说的话。

答案 2 :(得分:1)

事件采购和消息传递之间存在根本区别。

在消息系统中,消息很短暂。一些排队系统在从队列中拉出后保留了消息,但逻辑上它们已被处理并且可能被遗忘。

对于事件源,您明确设计从域模型发出的事件来表示聚合的状态。这些结构中的数据通常与使用服务总线发送的系统间类型消息有些不同。这只是因为您可能获得的系统消息信息比使用事件源恢复聚合状态所需的信息更多。

您ES商店中的事件始终处于正确的顺序。这应该使用聚合/流的版本强加。一个用户永远不应该#34;覆盖"另一个用户的事件,它甚至不可能。

要重新构建聚合,请使用ES商店。你不能也不应该依赖服务总线式的消息。

要构建投影,请使用事件存储。由于这些事件的顺序正确,因此您无需阅读任何特殊内容。事件存储应该具有跨越所有聚合的一些序列:

| aggregate id | data     | version | sequence number |
| ------------ | -------- | ------- | --------------- |
| 521          | {binary} | 1       | 1               |
| 521          | {binary} | 2       | 2               |
| 521          | {binary} | 3       | 3               |
| 516          | {binary} | 1       | 4               |
| 516          | {binary} | 2       | 5               |
| 7221         | {binary} | 1       | 6               |

即使事件流版本是每个聚合,序列号在所有中都是唯一的。通过这种方式,您可以从序列号1开始读取投影事件。

然后,您可以根据需要使用一些位置指示器进行多次投影:

| projection | position |
| ---------- | -------- |
| customer   | 2        |
| order      | 6        |

您的事件投影处理器将继续阅读事件并将其交给处理程序处理。处理程序将更新读取模型。位置光标将一直移动。

当您注意到在投影上出现了一些处理错误时,这非常方便。您可以删除您的读取模型并将位置重置为0以重新处理整个事件历史记录。

随时添加新投影也很容易。

我希望有所帮助。我有一个名为Shuttle.Recall的事件采购机制,如果你想寻找一些灵感:)