DDD:私域体育投注系统中正确选择Aggregate Root的困惑
问题描述
用户在私人联赛中参与体育投注,核心场景与规则如下:
- 针对每个赛事(Fixture),用户需为一组比赛选择预期结果与精确比分;赛事开始前可冻结自己的投注以查看其他用户的投注内容。
- 业务规则:
- 距离赛事首场比赛开始不足30分钟时,投注不可修改;
- 未提交所有比赛结果的情况下,投注不可冻结;
- 已冻结的投注不可修改。
- 每日结束时,模块收到完赛结果通知后,需完成所有投注结算、积分计算发放,并更新联赛排名。
最初设计以FixtureBets为聚合根,提供updateBets、freeze、resolveBets方法:调用resolveBets时计算积分并发布BetsResolved事件触发联赛排名重算。但存在明显问题:
- 每个联赛成员的投注结算都会触发一次排名重算,短时间内多次计算易导致数据不一致;
- 需追踪用户排名趋势(上升/下降),要求排名仅能重算一次,原方案性能与准确性都不达标。
现考虑将LeagueTable设为独立聚合根,等待所有用户积分更新完成后统一重算排名与趋势,询问该方案是否可行。
解决方案分析
这个方案完全可行,且能解决原设计的核心问题,具体落地思路如下:
1. 拆分聚合根,明确职责边界
FixtureBets:专注于单个用户在某一赛事下的投注生命周期管理,包括投注更新、冻结、根据完赛结果计算并更新用户积分,结算完成后发布UserPointsUpdated事件(仅携带用户ID、联赛ID、新增积分等必要数据)。LeagueTable:专注于联赛排名的计算、维护与趋势追踪,只负责处理排名相关的逻辑,不介入投注结算细节。
2. 用事件或批量触发统一排名重算
避免多次触发排名更新的关键是确保所有用户积分更新完成后再执行排名重算,可通过两种方式实现:
- 事件驱动型:
- 在
Fixture聚合根中维护一个「已结算投注计数器」,每完成一个用户的FixtureBets结算,就递增计数器; - 当计数器等于联赛总成员数时,发布
AllBetsResolvedForFixture事件; LeagueTable监听该事件,触发recalculateRankings方法执行统一重算。
- 在
- 批量处理型:
每日结束时,先批量完成所有赛事下的FixtureBets结算,待所有用户积分更新完成后,直接调用LeagueTable的排名重算接口。
3. 实现排名趋势追踪
在LeagueTable执行recalculateRankings时,按以下步骤生成趋势:
- 重算前,先对当前联赛的用户排名做快照(存储用户ID与当前排名);
- 基于更新后的所有用户积分,计算新的联赛排名;
- 将新排名与快照对比,为每个用户生成排名趋势(上升/下降/不变),并持久化到用户的趋势记录中。
4. 解决一致性与性能问题
- 统一重算避免了并发场景下多次更新排名导致的数据冲突,确保排名数据唯一且准确;
- 单次批量计算排名比多次触发计算的性能更优,尤其在联赛成员较多的场景下。
内容的提问来源于stack exchange,提问作者fxeuph
相关产品推荐
相关产品推荐

