当系统从事件存储中重新还原(rehydrates)聚合实例时,它必须加载并重播与该聚合实例关联的所有事件。这里可能的优化是存储聚合状态在最近某个时间点的滚动快照,以便系统只需要加载快照和后续事件,从而减少必须重新加载和重播的事件数量。在Contoso会议管理系统中,随着时间的推移,唯一可能会累积大量事件的聚合是可用座位(SeatsAvailability)聚合。我们决定使用Memento模式作为快照解决方案的基础,以便与可用座位(SeatsAvailability)聚合一起使用。我们实现的解决方案使用一个memento来捕获座位可用性聚合的状态,然后在缓存中保存一个memento的副本。然后,系统尝试处理缓存的数据,而不是总是从事件存储中重新加载聚合。
Gary(CQRS专家)发言:
通常,在事件源上下文中,快照是持久化的,而不是我们在项目中实现的临时本地缓存。
就提高系统中事件消息的吞吐量而言,并行发布事件被证明是最重要的优化之一。为了得到最好的结果,团队进行了多次迭代:
迭代1:这种方法使用并行。使用Parallel.ForEach方法和自定义分区(把消息分配到分区中),并设置并行度的上限。它还使用同步的Azure服务总线API调用来发布消息。
迭代2:这种方法使用了一些异步API调用。它需要使用基于自定义信号量的节流来正确处理异步回调。
迭代3:这种方法使用动态节流,它考虑到顺时故障,这些故障表明向特定Topic发送了太多的消息。这种方法使用异步的Azure服务总线API调用。
Jana(软件架构师)发言:
当系统从服务总线检索消息时,我们在SubscriptionReceiver和SessionSubscriptionReceiver类中采用了相同的动态节流方法。
另一个优化是向Azure服务总线Topic订阅添加过滤器,以避免读取那些稍后将被与订阅关联的处理程序忽略的消息。
Markus(软件开发人员)发言:
这里我们利用了Azure服务总线提供的特性。
这使可用座位(SeatsAvailability)聚合的接收者能够使用支持会话的订阅。这是为了确保每个聚合实例只有一个写入者,因为可用座位(SeatsAvailability)聚合是一个高争用的聚合。这阻止了我们在扩展时接收大量并发异常。
Jana(软件架构师)发言:
在其他地方,我们使用带有会话的订阅来保证事件的顺序。在本例中,我们使用会话是出于不同的原因——以确保每个聚合实例只有一个写入者。
这个优化缓存了会议web网站到处使用的几个读模型。它包含逻辑来决定如何基于特定会议的可用座位的数量来保持缓存中的数据:如果有很多空位,系统可以缓存数据很长一段时间,但是如果很少有空位就不缓存数据。
划分服务总线团队还对服务总线进行了划分,以使应用程序更具可伸缩性,并避免在系统发送的消息量接近服务总线能够处理的最大吞吐量时进行节流。每个服务总线Topic可以由Azure中的不同节点处理,因此通过使用多个Topic,我们可以增加潜在的吞吐量。我们考虑了以下分区方案:
为不同的消息类型使用不同的Topic。
使用多个相似的Topic,并以循环方式监听和读取它们,以分散负载。
有关这些划分方案的详细讨论,请参阅Martin L. Abbott和Michael T. Fisher所写的《可伸缩性规则:Web站点伸缩的50个原则》(Addison-Wesley, 2011)中的第11章“异步通信和消息总线”。
我们决定为订单聚合和可用聚合发布的事件使用单独的Topic,因为这些聚合负责了通过服务总线流动的大多数事件。
Gary(CQRS专家)发言:
并不是所有的信息都具有相同的重要性。您还可以使用消息总线来处理单独的、按优先级排列的不同的消息类型,甚至可以考虑不为某些消息使用消息总线。
Jana(软件架构师)发言:
将服务总线与系统的任何其他关键组件一样对待。这意味着您应该确保您的服务总线可以伸缩。此外,请记住,并非所有数据对您的业务都具有相同的价值。仅仅因为您有一个服务总线,并不意味着所有东西都必须经过它。明智的做法是消除低价值、高成本的流量。