我目前正在设计新系统我坚信CQRS + ES非常适合。我想验证我的"大规模"设计预设听起来不错,而且我没有走错方向。
对我而言,由于一致性和网络边界的一致性,将每个聚合根(写模型)置于其自己的微服务中似乎是一个好主意。
由于一致性保证,我认为可以安全地假设每个聚合实例可能只有自己的事件流。在实践中,为每个聚合实例清理事件存储将是一种矫枉过正,但在相同聚合类型的微服务之间共享一个或几个复制的事件存储对我来说似乎是理智的选择。如果不需要复制,我们甚至可以根据聚合ID对事件存储进行分片。
这样,由于聚合类型范围的微服务很少,每个处理大量聚合实例的命令,扩展应该对系统的其余部分足够透明。
然后我有理由让Projectors(阅读模型)也在他们自己的微服务中,每个都有自己的DB,应该在相同类型的投影机之间共享。
因为投影仪'查询界面不是外部世界友好的(我假设投影仪提供类似于存储库的界面来发出查询,在我看来,它不能很好地与访问控制,速率限制等),每个投影仪应该提供一些统一的网络接口,供BFF(后端前端)使用,实际服务于某些API端点,确保安全性,提供版本控制等。
tl; dr:我提供了上面(环绕)的图形表示,以便用糟糕的绘图补偿我的不良措辞。 PS:重放服务是监视新事件的服务,因为它们被附加到事件存储中,并将它们广播到感兴趣的订阅投影仪或流程管理器(未绘制),或者为陈旧或空数据库重放整个事件序列。
这个CQRS +微服务调整听起来不错,或者我从根本上误解了什么,整个设计都是垃圾?
UPD1:
如果我生成多个相同类型的投影机实例来处理来自繁重查询的额外负载,他们将如何监听事件?对我来说,分配一个实例来完成所有事件处理工作,更新数据库等等似乎很奇怪,因为随着负载的增加,它可能会过载。因此,分配事件处理也是有意义的,对吗?
此外,虽然我一直在写这篇文章,但我是否将投影仪进一步分拆为投影机编写者"是不是一个好主意。 (那些在DB中监听事件和更新共享状态的人)和"投影仪阅读器" (那些听取查询和返回状态的人),数据库作为真相和合并点的来源。通过这种方式,我们可以无偿地扩展非对称负载(小事件,大量查询)。
其中一个必要条件是防止不同的投影机编写器实例同时处理来自同一聚合的事件,因为使用无序事件更新表示将导致内部一致性和即时灾难的丢失。
对于"如何",我可以想到几个解决方案:
为所有传入事件保留单个RabbitMQ队列,并使所有投影仪使用带有确认的队列中的事件。更新数据库后,投影机编写器会向RabbitMQ发出确认,并从队列中丢弃事件。否则,如果投影机编写器因某种原因死亡,则事件将再次重新排列到下一台投影机中。
对于每个聚合,我们应该保留高度/修订号,并且只有当下一个修订号(包含在事件中)正好比当前修订号多一个时才允许UPDATE成功。如果这个条件不成立,那么重新排列这个事件,希望不一致将在那时解决,然后抓住下一个。
最终,它将完成,并且给定足够数量的聚合,它不会需要重新排队。
提供某种调度服务来监听事件,每个投影机类型一个调度员。此调度程序应根据聚合ID的散列将事件分发给投影机编写器,因此,相同的聚合始终由具有索引hash(event.aggregateId) % numberOfProjectorWriters
的相同聚合处理。
这将永远不会重新排队,但会提前终止MQ,引入单点故障,并且如果由于某些节点死亡或动态扩展而导致投影机编写器数量发生变化,或者... ...
以某种方式使用标题交换来实现#1和#2的组合,以使消费者更喜欢同一组聚合ID,但如果消费者数量在中间变化则不会搞砸。
< / LI> 醇>答案 0 :(得分:0)
我相信,虽然技术上聚合的根可以存在于他们自己的微服务中,但这种粒度级别可能会带来不必要的复杂性。通常他们会说,从设计良好的巨石开始。
通常,如果BC中有一些聚合,它们可能共享一些服务,存储库,因此这些聚合一起可以显着降低复杂性,并且可以构成一个有凝聚力的组件。但这可能取决于扩展需求。
顺便说一句,您的意思是什么事件存储一致性保证?事件流最有用的保证是事件的排序。这在分布式环境中真的很难实现。如果您为候选事件商店提供此类保证的链接,那就太棒了。
我同意你的看法,读取的模型可能位于不同的微服务中,需要单独进行缩放。实际上,读模型管理器可以是单独的反应式微服务。但是这些管理器生成的读取模型是普通资源,可以使用简化的REST API存储在单个读取优化,扩展和分片的资源存储中,其中读取模型管理器只是PUT准备资源,用一些超时标记它们并且其他元数据。
不仅如此,聚合也可能具有REST API。命令和事件可以是这种API所服务的资源。您可以将命令发布到聚合,获取命令结果的URL,聚合的GET事件或聚合类的GET合并事件流...