【问题标题】:How to create a ddd aggregate and update other aggregate with its reference in the same transaction?如何在同一事务中创建一个 ddd 聚合并使用其引用更新其他聚合?
【发布时间】:2015-03-02 12:32:31
【问题描述】:

我想问一下我在游戏实现中使用 DDD 时遇到的问题。

我有一种情况,根据系统收到的消息,为玩家创建了一个新的聚合。问题来自于这个新聚合必须由另一个聚合(表聚合)使用其 id 引用。第二个聚合保留了一个值对象,其中包含有关牌桌上玩家的信息,例如他的位置或选择的颜色。玩家聚合存储玩家动作,因为根据玩家的数量和他们发出的动作,表聚合可能会变得庞大,如果我将所有内容都保留在表上,数据库中的锁争用成本会太高。为了让玩家能够加入表格,必须首先通过一些验证,例如未采用颜色、已达到最大玩家数量或玩家已处于非活动状态的游戏,这就是为什么此值对象与信息一起存储在表格中的原因来自玩家。

因此,基于此,我仅在玩家发出要存储在表外的第一个动作时才创建玩家聚合。这样做的问题是该表可能包含引用尚未创建的聚合的值对象,这使得代码看起来很难看,因为它迫使它检查空引用。另一种选择,即首先创建玩家集合,也破坏了设计,因为创建玩家的验证首先发生在桌子上。由于系统的并发性,首先在表上进行验证,然后创建玩家集合,然后在接收到来自创建的事件时更新表,这可能会导致许多竞争条件。我唯一能想到的是在同一个事务中更新两个聚合,但这违反了 DDD 规则。

有什么好的解决办法吗?我想这最终将与设计有关,但我想不出任何方法可以在不引入性能问题的情况下工作。

谢谢。

【问题讨论】:

  • player = table.newPlayer(...); playerRepository.save(player); tableRepository.save(table); 之类的东西呢,Table 只是将 VO 添加到内部集合中。在这里,您不要在同一个事务中修改两个聚合,因为播放器是刚刚创建的,而不是修改的。因此,播放器实例上不能存在争用。
  • 你真的能做到吗?我认为不修改同一事务中的两个聚合的全部内容也包括创建。即使这在我的情况下也行不通,因为一旦离开表格,玩家就不会从 PlayerRepository 中删除。我必须检查是否这样做,我不知道我是否会破坏其他东西。还有另一个要求为玩家存储快捷方式,但这可能在另一个有界上下文中,因此在玩家离开后移除玩家仍然是安全的。但我想先知道在 ddd 中是否同时更新和创建有效。
  • 是的,我相信它是有效的,因为不会增加并发冲突的风险。例如。无论您只修改 Table 聚合还是修改 Table 聚合并创建 Player 对象并保存它,都不会在并发冲突方面发生任何变化。并发冲突是避免在单个事务中修改多个聚合的主要原因。
  • 感谢您的提示,真正理解 DDD 以及什么是允许的,什么是不允许的并不是一个容易的主题。

标签: java domain-driven-design


【解决方案1】:

通常,您的部分问题可以通过从领域和无处不在的语言角度来解决。

  • Udi Dahan 有一个 excellent article 说明聚合根不是凭空出现的,而是作为必须是一流的无处不在的语言公民的域操作的结果。在您的情况下,Player 可能不是由应用程序服务创建,而是由Table AR 最接近域源创建。

    请注意,我并不一定主张像 Udi 的示例中那样保留对其他根的完整引用,但您可以将其替换为 Table.ReceivePlayer(...) 返回新创建的播放器,然后可以通过应用程序服务将其添加到 PlayerRepository .由于Player 不扮演聚合根(即修改和不变执行的单一入口点)的角色,而是在该场景中扮演一个简单实体的角色,因此您可以合法地将所有这些包装在一个事务中。

  • 推动聚合建模的力量基本上是真正的不变量、并发性和性能。为了证明您的建模方式的合理性,您声明表聚合可能会变得庞大,如果我将所有内容都保留在表上,那么数据库中的锁争用成本将太高

    我认为,多探索一下这种争论的真正含义并将领域术语置于其背后,从而丰富您无处不在的语言,这将是值得的。表是大规模并发的吗?怎么会这样 ?游戏是否有玩家修改的可变共享状态?玩家是否需要根据其他玩家的动作来做出决定,如果同时采取并发动作会引入某种偏见?所有这些在领域术语中是如何翻译的?额外的Player 聚合给你带来了什么?

    就性能而言,庞大的Table 聚合会是什么样子?它将如何影响系统?

  • “没有跨越多个聚合的事务”规则只是将聚合作为一致性边界的必然结果,它不是一成不变的。制作小的、独立一致的聚合并在单个事务中对其中的多个进行大量修改是没有意义的,因为您会创建不必要的争用,并且随之而来的是潜在的并发问题。但是在除了唯一的Table AR 之外不存在并发问题的情况下,创建肯定就是这种情况,因为没有其他人知道添加了播放器,就好像您只修改了一个聚合而且这条规则不再适用了。

    如果您在 99% 的修改案例中遵守规则,但在 1% 的创建案例中不遵守规则,那么您仍然在排队。

【讨论】:

  • 关于泛在语言的问题,我已经思考了好几次,并且我认为系统可能遭受性能问题的点已经很好地确定了。我可能没有做的是考虑它的正确名称以及它应该如何工作。争论来自这样一个事实,赌注不能全部存储在同一个集合中。正如我所说的那样,这样做会导致一张巨大的牌桌,因为可能有数百张牌桌,玩家可能会同时下注。但是第二个聚合应该反映的是这个存储,所以它可以被称为 PlayerBetsBook
  • 在此之后,我认为现在应该使用播放器创建和删除以这种方式执行的第二个聚合。这将解决引用尚未创建的内容的问题,因为这不会再发生了。但是,在同一事务中创建它不是一种选择。即使不违反任何 DDD 规则,我使用的 MongoDB 在处理事务性时也无济于事。所以我会坚持懒惰的创作,在玩家离开时添加移除来解决我的设计问题。感谢您的解释,它真的很有帮助。
  • 不客气。现在我明白了为什么第二个聚合 - 完美地说明了为什么您应该始终在 Q 中尽可能多地描述上下文:) 结果您的实现接近“拆分热聚合”策略:jefclaes.be/2014/11/splitting-hot-aggregates.html。您是否有任何“协调 PlayerBetsBook 与表”操作发生在间隔时间,例如在该博客文章中?
  • 是的,我有类似的东西。在游戏结束时,我必须根据结果计算谁赢了什么,所以我必须遍历坐在桌子上的玩家的 PlayersBets 列表。完成后,我删除所有赌注,然后开始新游戏。因此,那时将所有内容都放在桌面上会更容易,因为我不必为每个玩家访问一次数据库,但这在游戏期间性能不够。
猜你喜欢
  • 2023-02-26
  • 2011-01-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-11-11
  • 2019-12-04
相关资源
最近更新 更多