实施跨多个聚合的严格一致性

时间:2017-06-07 13:15:35

标签: domain-driven-design aggregate eventual-consistency

考虑以下业务要求:

我们有玩家可以玩游戏。玩家一次只能玩一个游戏。游戏需要两名玩家。

系统将包含数百万玩家,游戏大约需要两分钟。可能会出现并发问题。

我们希望遵守单笔交易涉及单一汇总的规则。此外,最终的一致性不得导致接受的游戏,由于并发问题,必须在之后取消(即使在很短的时间内)。因此,最终的一致性并不合适。

我们如何定义聚合及其边界以强制执行这些业务规则?

我设想了两种方法:

1。基于事件的握手

汇总Player,汇总Game

当请求游戏时,它会推送GameRequested - 事件。 Player订阅此活动并回复相应的活动,GamePlayerAcceptedGamePlayerRejected。只有两个Player都已接受,Game才会开始(GameStarted)。

优点:

  • 汇总Player负责管理与域模型相对应的自己的可用性

缺点:

  • 启动Game的责任分散在多个聚合中(似乎是“假的” - 一致性)
  • 很多沟通开销
  • 需要采取一致性措施,例如:如果出现问题,可以释放Player

2。收集聚集

汇总Player,汇总GamesManager(包含值对象ActiveGamePlayers的集合),汇总Game

要求GameManager以两个给定的Game开始新的PlayerGameManager能够确保Player一次只播放一次,因为它只是一个聚合。

优点:

  • 没有执行一致性的事件,例如GamePlayerAcceptedGamePlayerRejected

缺点:

  • 域模型似乎模糊不清
  • Player管理可用性的责任已转移
  • 我们必须确保只创建一个GameManager实例,并引入域名机制,让客户不必担心中介聚合
  • 独立Game - 因为GameManager - 聚合锁定自身而开始相互破坏
  • 需要进行性能优化,因为GameManager - 聚合会收集所有活跃的游戏玩家,这将是数千万

似乎这些方法都不适合解决问题。我不知道如何设置边界以确保模型的严格一致性和清晰度以及性能。

1 个答案:

答案 0 :(得分:2)

我会选择基于事件的握手,这就是我要实施的方式:

根据我的理解,您需要将Game流程实施为Saga。您还必须定义Player汇总,RequestGame命令,GameRequested事件,GameAccepted事件,GameRejected事件,{{1命令,MarkGameAsAccepted命令,MarkGameAsRejected事件和GameStarted事件。

因此,当GameFailed想要使用Player A玩游戏时,Player B会收到Player A命令。如果此播放器正在播放其他播放器,则会引发RequestGame异常,否则会引发PlayerAlreadyPlaysAGame事件并将其内部状态更新为GameRequested

playing saga捕获Game事件并将GameRequested命令发送到RequestGame聚合(这是一个Player B聚合{{1}等于Player)。然后:

  • 如果ID正在玩另一个游戏(它通过查询其内部A状态知道这一点),那么它会引发Player B事件; playing saga捕获此事件并向GameRejected发送Game命令;然后MarkGameAsRejected引发Player A事件并将其内部状态更新为Player A

  • 如果GameFailed没有玩其他游戏,则会引发not_playing事件; Player B saga捕获此事件并将GameAccepted命令发送到Game聚合; MarkGameAsAccepted然后发出Player A事件并将其内部状态更新为Player A

为了理解这一点,您应该尝试对用例进行建模,就像没有计算机存在一样,玩家将是通过打印邮件进行通信的人。

此解决方案具有可扩展性,我知道这是必需的。

其他解决方案对于数百名玩家来说似乎并不可行

第三个解决方案将使用SQL表或NoSQL集合中的活动播放器的集合,而不使用聚合战术模式。为了确保这一点,当将一对玩家设置为活动时,您可以使用optimistick锁定或支持(低可伸缩性)或两阶段提交(有点丑陋)的事务。