聚合、不变量与事务边界
聚合把必须同时保持一致的一组对象放进同一个修改边界,并由聚合根维护其中的不变量。
1. 从不变量确定聚合
不变量是在一次业务操作完成后必须成立的规则。例如:
- 订单总金额必须等于有效订单项金额之和;
- 已取消的订单不能继续确认;
- 账户可用余额不能小于允许的下限;
- 一个预订不能占用超过可售数量的库存。
聚合的范围由这些规则决定。一起展示的数据、数据库中的外键关系或页面上的表单,并不能直接证明对象属于同一个聚合。
设计时先写出命令和不变量,再寻找能够负责检查规则并完成状态变化的对象。没有需要共同维护的规则,就没有必要把对象装进一个大聚合。
2. 聚合根是修改入口
聚合根是外部持有和修改聚合的入口。外部代码通过聚合根的方法表达业务意图,例如:
order.changeQuantity(productId, quantity);
order.confirm(confirmTime);
order.cancel(reason);
这些方法同时完成权限范围内的状态检查、值计算和领域事件记录。订单项不能被仓储或 Controller 绕过订单直接修改,否则订单总额和状态规则无法集中保证。
聚合内部对象仍然可以有行为。聚合根负责控制访问边界,不需要亲自实现全部计算。
3. 一次事务通常修改一个聚合
聚合是事务一致性的候选边界。应用服务加载一个聚合,调用业务方法,再以一个数据库事务保存它。事务提交后,聚合内的不变量已经成立。
如果一条业务命令经常要求同时锁住多个聚合,需要重新检查:
- 它们是否原本属于同一个一致性边界;
- 规则是否可以改为预留、额度或补偿流程;
- 是否把“立刻可见”误当成“必须同事务提交”;
- 当前边界是否让一个聚合承担了过多业务职责。
跨聚合业务通常通过应用服务编排和领域事件推进。每一步维护自己的本地不变量,整体流程接受明确的中间状态,并准备重试、补偿和对账。
4. 聚合之间使用标识引用
一个聚合引用另一个聚合时,优先保存对方的 ID 或必要快照,而不是在内存中形成可任意遍历和修改的对象图。
final class Order {
private CustomerId buyerId;
private List<OrderLine> lines;
}
订单需要收货人姓名时,可以保存下单时快照;需要查询会员等级时,由应用服务向会员上下文获取。直接持有并修改 Customer,会把两个聚合的生命周期和事务绑在一起。
5. 边界过大会放大冲突
大聚合看起来能提供更强的一致性,同时会带来这些代价:
- 每次操作加载和保存更多数据;
- 无关操作争用同一个版本或锁;
- 一个热点根对象限制并发;
- 任何子对象变化都可能影响整个聚合的发布和缓存。
例如把“客户的全部订单”放进客户聚合,客户下单、修改地址和查看权益都会争用同一个根对象。订单具有独立生命周期时,应让每个订单成为独立聚合。
6. 边界过小会把规则推到外部
如果每张表都对应一个聚合,业务规则会散落在应用服务、数据库触发器和调用顺序中。一次订单修改可能先更新订单项,再重新计算订单总额;任一步失败都可能留下不合法状态。
以下迹象说明边界可能过小:
- 每次修改都要按固定顺序加载多个对象;
- 多个应用服务重复同一组校验;
- 数据库需要大量跨对象约束才能维持业务规则;
- 任何一个对象单独存在都没有业务意义。
7. 并发控制保护不变量
聚合不能只在内存中检查规则,还要处理并发写入。常见方式是乐观锁:保存时带上版本号,只有版本未变化才更新;冲突后重新加载并根据命令语义决定重试。
UPDATE orders
SET status = ?, version = version + 1
WHERE id = ? AND version = ?;
受热点竞争影响的操作可以使用数据库锁、原子条件更新或按业务键串行化。重试前要确认命令幂等,不能把已经产生的外部副作用重复执行。
8. 常见问题
8.1 聚合是否对应一张表
没有固定对应关系。一个聚合可以持久化到多张表,一张表也可能只是读取模型的一部分。聚合按业务一致性划分,表按查询、约束和存储效率设计,两者通过仓储映射。
8.2 领域事件应该在事务前还是事务后发布
聚合先记录事件,应用层在本地事务成功后可靠发布。需要保证数据库状态与事件不丢失时,可以使用 transactional outbox;事件消费者仍要幂等。直接在事务中调用远程服务,会把本地锁持有时间和远程故障绑定在一起。
9. 面试题
9.1 怎样确定聚合边界,聚合过大或过小分别有什么问题
出现公司:阿里巴巴
考察重点
- 不变量、聚合根与事务边界。
- 跨聚合协作和最终一致性。
- 大聚合的争用与小聚合的规则泄漏。
相关内容:第 1 节“从不变量确定聚合”至第 7 节“并发控制保护不变量”。
参考回答
我会先列出一条命令结束时必须成立的不变量,再把必须在同一事务中维护这些规则的实体和值对象放在一个聚合内,由聚合根作为修改入口。聚合之间只保存标识或必要快照,跨聚合流程由应用服务和事件推进,并明确中间状态、幂等、补偿和对账。
边界过大会增加加载成本、锁冲突和热点,独立生命周期的对象也无法单独变化;边界过小会让同一条规则散落到多个服务和调用顺序中,失败后容易留下不一致。最后还要用真实并发模式验证边界,选择版本号、条件更新或锁来保护不变量。