考虑以下业务要求:
我们有玩家可以玩游戏。玩家一次只能玩一个游戏。游戏需要两名玩家。
系统将包含数百万玩家,游戏大约需要两分钟。可能会出现并发问题。
我们希望遵守单笔交易涉及单一汇总的规则。此外,最终的一致性不得导致接受的游戏,由于并发问题,必须在之后取消(即使在很短的时间内)。因此,最终的一致性并不合适。
我们如何定义聚合及其边界以强制执行这些业务规则?
我设想了两种方法:
1。基于事件的握手
汇总Player
,汇总Game
。
当请求游戏时,它会推送GameRequested
- 事件。 Player
订阅此活动并回复相应的活动,GamePlayerAccepted
或GamePlayerRejected
。只有两个Player
都已接受,Game
才会开始(GameStarted
)。
优点:
Player
负责管理与域模型相对应的自己的可用性缺点:
Game
的责任分散在多个聚合中(似乎是“假的” - 一致性)Player
2。收集聚集
汇总Player
,汇总GamesManager
(包含值对象ActiveGamePlayers
的集合),汇总Game
。
要求GameManager
以两个给定的Game
开始新的Player
。 GameManager
能够确保Player
一次只播放一次,因为它只是一个聚合。
优点:
GamePlayerAccepted
,GamePlayerRejected
等缺点:
Player
管理可用性的责任已转移GameManager
实例,并引入域名机制,让客户不必担心中介聚合Game
- 因为GameManager
- 聚合锁定自身而开始相互破坏GameManager
- 聚合会收集所有活跃的游戏玩家,这将是数千万似乎这些方法都不适合解决问题。我不知道如何设置边界以确保模型的严格一致性和清晰度以及性能。
答案 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锁定或支持(低可伸缩性)或两阶段提交(有点丑陋)的事务。