大众运输:在存在不同消息类型时确保消息处理顺序

时间:2019-03-05 19:47:01

标签: rabbitmq cqrs servicebus event-sourcing masstransit

我是地铁的新手,我想了解一下它是否对我的情况有帮助。 我正在构建使用CQRS事件源体系结构实现的示例应用程序,并且需要服务总线以便将由命令堆栈创建的事件调度到查询堆栈非规范化器。

我们假设在我们的域中有一个聚合,我们将其称为照片,以及两个不同的域事件: PhotoUploaded PhotoArchived

鉴于这种情况,我们有两种不同的消息类型,默认的大众运输行为是创建两种不同的RabbitMq交换:一种用于PhotoUploaded消息类型,另一种用于PhotoArchived消息类型。

让我们假设有一个称为 PhotoDenormalizer 的反规范化器:该服务将同时使用两种消息类型,因为无论何时上传或归档照片,都必须更新照片读取模型。

在默认的大众运输拓扑下,将有两种不同的交换方式,因此无法保证不同类型事件之间的消息处理顺序:我们唯一的保证是,所有相同类型的事件将按顺序处理,但是我们不能保证不同类型事件之间的处理顺序(请注意,鉴于我的示例的事件语义,处理顺序很重要)。

如何处理这种情况?地铁适合我的需求吗?我是否完全不了解域事件调度的要点?

1 个答案:

答案 0 :(得分:6)

免责声明:这不是您问题的答案,而是一个预防性消息,说明为什么您应做您打算做的事情。

尽管RMQ之类的消息代理和MassTransit之类的消息中间件库非常适合集成,但我强烈建议您不要将消息代理用于事件源。我可以参考我的旧答案Event-sourcing: when (and not) should I use Message Queue?来解释其背后的原因。

您发现自己的原因之一-事件顺序将永远无法保证。

另一个明显的原因是,根据通过消息代理发布的事件来构建读取模型,这有效地消除了重播的可能性,并消除了需要从头开始处理事件的新读取模型,但是它们得到的全部是正在现在发布的事件。

聚合形成事务边界,因此每个命令都需要确保它在一个事务中完成。尽管MT支持transaction middleware,但是由于RMQ不支持事务,因此它仅保证针对支持它们的依赖项获得事务,而不能保证消费者主体中的context.Publish(@event)获得事务。您很有可能提交更改,而不会在读取端获取事件。因此,事件存储的经验法则是,您应该能够从存储中预订更改流 ,并且不发布代码中的事件,除非这些事件是集成事件而不是域事件。

对于事件源,至关重要的是每个读取模型在其计划的事件流中保留其自己的检查点。消息代理没有提供这种功能,因为“检查点”实际上就是您的队列,并且一旦消息从队列中消失了-它永远消失了,就再也没有回来。

关于实际问题:

您可以使用message topology configuration为不同的消息设置相同的实体名称,然后将它们发布到同一交换所,但这属于Chris在该页面上写的“滥用”类别。我没有尝试过,但是您绝对可以尝试。消息CLR类型是元数据的一部分,因此不应该存在反序列化问题。

但是同样,将消息放入同一交换不会为您提供任何订购保证,除非所有消息都将排入使用服务的队列中。

您将至少必须根据您的聚合ID设置分区过滤器,以防止并行处理同一聚合的多个消息。顺便说一下,这对于集成也很有用。我们就是这样做的:

void AddHandler<T>(Func<ConsumeContext<T>, string> partition) where T : class
    => ep.Handler<T>(
        c => appService.Handle(c, aggregateStore), 
        hc => hc.UsePartitioner(8, partition));

AddHandler<InternalCommands.V1.Whatever>(c => c.Message.StreamGuid);