CQRS - 何时发送确认消息?

17

例子: 商业规则规定,当下单时客户应该收到确认消息(电子邮件或类似物)。

假设从领域中派发了一个NewOrderRegisteredEvent,并被事件侦听器捕获以发送确认消息。在完成此操作后,某些其他事件处理程序抛出异常或其他错误导致工作单元回滚。现在我们向用户发送了一条关于已回滚的内容的确认消息。

采用"CQRS"方式如何解决这种想要在工作单元提交后执行某些操作的问题?另一个复杂因素是重放事件。我不希望在重放记录的事件以构建新视图/投影时重新发送旧的确认消息。

我目前最好的理论:我刚开始探索 CQRS 这个迷人的世界,想知道这是否会作为 saga 实现?如果 saga 就像一个状态机,每个转换只能发生一次,那么我想这就可以解决这个问题?但我很难想象这将与命令总线和领域事件结合在一起。


1
将“发送电子邮件”任务作为消息处理。如果工作单元回滚,则消息分发(到持久化存储)也会回滚。其他东西会接收该消息并执行电子邮件操作。顺便说一下,这与CQRS无关,是普通的分布式计算常识。 - Yves Reynhout
关于事件重放,它绝不会引起这种行为。如果您的实现有这种情况发生,那么您的做法是错误的。 - Yves Reynhout
这很有道理,但我对CQRS上下文中的这个很感兴趣。什么是“消息”?谁/什么会在何时拾取它?很好利用事件存储来避免引入另一个存储机制来跟踪出站通信。 - Kimble
1
您的事件存储是您的队列。然而,实际发送电子邮件的代码需要通过拉取或推送的机制/技术进行通知。 - Yves Reynhout
@YvesReynhout:这实际上是我几天前才意识到的。我喜欢这个想法的原因是可以消除事件存储和事件消费者之间的任何消息总线。这确实使每个消费者负责跟踪它已经处理和未处理的内容,但为了消除消息总线,这是一个小代价! - Kimble
2个回答

18
  1. 事务完成后才应该发生事件。如果出现任何问题并且回滚,那么从外部角度来看就没有发生事件。因此,根本不应该发布它。但是如果需要,可以发布一个OrderRegistrationFailed事件。

  2. 除非命令已成功执行,否则不应该发送邮件。

    首先解释一下为什么命令处理程序(如另一个答案中所提出的)不是正确的位置:在某些情况下,命令处理程序无法确定命令是否最终会成功。让命令处理程序调用发送邮件操作也会将过程知识放入命令处理程序中,这将破坏SRM并过于紧密地将业务规则与应用程序层耦合在一起。

    应该是在事件处理程序中后发送邮件,即在事实发生后。

    为了防止重播期间触发此处理程序,您只需不注册它即可。这类似于测试应用程序的方式。您只注册实际需要的处理程序。

    • 生产系统 - 注册所有事件处理程序
    • 测试 - 仅注册经过测试的事件处理程序
    • 重播 - 仅注册投影/去规范化处理程序

另一个更松散耦合但更复杂的可能性是让Saga处理NewOrderRegisteredEvent并向适当的有界上下文发布SendMail命令(感谢Yves Reynhout在问题的评论中指出这一点)。


1
这是我在stackoverflow上得到的最好的答案之一!因此,域事件将被缓冲在某个地方,仅在事件已成功写入事件日志后才发布到域外(到投影/去正规化器)?我猜写入事件存储通常发生在由命令处理程序/拦截器初始化的事务内部? - Kimble
基本上是的,一个或多个事件的存储(无论是在事件日志还是事件存储中,请注意不要混淆两者)将是交易的最后一部分。只有在成功存储之后,它们才会被分派和处理,可能甚至是异步的。 - Dennis Traub
事件日志和存储之间有什么区别?我相当确定在阅读有关CQRS的文章时,它们被交替使用过。 - Kimble
3
事件日志就是一个表格或文件,用于存储事件以供参考和重播。领域模型的持久化仍然可以基于状态,例如在关系型数据库中。可能的进一步步骤是不从状态中存储和重现领域模型,而是从事件本身中重现。这就是所谓的事件溯源。在这种情况下,我们将使用事件存储,而不是事件日志。虽然技术上事件日志也是一种存储方式,但通常会造成混淆:只使用事件日志与使用事件溯源模式之间会有区别。 - Dennis Traub
如果Saga在调用命令后触发事件,这样做是否可以? - Eugene

3
有两种可能的解决方案:
1)事件的发布和处理(即电子邮件)是单个事务的一部分,因此您的事务框架会为您处理。 如果电子邮件发送失败,则事件将被回滚并重试该命令。 这在概念上很清晰易懂。但实际上,这可能很麻烦,因为它通常涉及分布式事务,而这样的事务难以实现。 您的电子邮件客户端能否加入保存事件的数据库相同的事务中?
2)事件的发布是事务性的,但每个事件处理程序都以自己的方式处理事务。 发送电子邮件的事件处理程序可以跟踪其已看到的事件。 如果事件处理程序崩溃,则会请求旧事件并处理它们。 您可以根据业务决策确定如果人们缺少或重复电子邮件会有多大问题。(对于涉及货币的交易,答案可能是不允许出现问题。)
解决方案(2)通常是DDD / CQRS圈子中推广的,因为它是更松散耦合的解决方案。 在存储事件的数据库和投影不经常更改的小型系统中,解决方案(1)非常实用。 方案(2)允许各种事件处理程序以自己的方式工作。 方案(1)可能会导致许多不重叠的问题纠缠在一起。 在这种情况下,您的订单业务规则直到电子邮件的所有诡异事件都得到处理才会完成。 这可能会使您的系统变得缓慢。
如果发送电子邮件更有趣而不仅仅是“看到事件,发送电子邮件”,那么您可能需要使用saga或workflow。 在大型操作中,电子邮件通常本身就是一个复杂的系统,您不太可能需要实现大部分功能。 只需确保将电子邮件置于某种请求队列中(使用方法(2)),电子邮件系统就很可能进行重试/批处理/防止垃圾邮件/在夜间工作等操作。

谢谢您的详细回答!分布式事务确实很棘手。关于第二种解决方案,这是否涉及引入新的存储机制来跟踪已发送的电子邮件?或者通常也会利用事件存储来解决这个问题吗?我看到一些潜在的问题是事件处理程序发布它们自己的事件...特别是在回放期间,“发送电子邮件x”事件将在触发电子邮件的事件之后出现。 - Kimble
如果您想要真正的事务性电子邮件发送,那么您必须以某种方式处理它。也许通过将成功发送的电子邮件的身份保存到磁盘中。但是,如果您将需要的消息写入某个队列,大多数商业级电子邮件操作是否保证交付? - Sebastian Good
真正的事务性电子邮件发送目前超出了范围。我只是好奇在cqrs中如何处理这个问题,特别是与事件重放相结合。但是,将电子邮件放入队列并在事件存储中存储相关ID将大大保证交付。 - Kimble

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接