聚合
聚合是一组由聚合根维护业务规则的对象。本文讲解不变量、事务边界和并发写入。
1. 面试核心
先记住这几条,再读正文- 聚合边界。 聚合按业务不变量划分,由聚合根统一接收修改命令。卡 · 综合题
- 事务边界。 聚合内的状态变化在同一本地事务中提交。
- 聚合间引用。 聚合间使用 ID 引用,独立生命周期降低共同修改的成本。
- 并发更新。 乐观锁用版本条件拒绝旧状态写入,冲突后重新校验业务规则。卡 · 版本冲突与命令重放
- 实体与值对象。 实体按身份识别,值对象按属性比较,聚合根本身是实体。卡 · 实体与值对象
2. 不变量与聚合
不变量是业务操作结束后必须成立的规则。例如,订单总额等于有效订单项金额之和。取消后的订单保持取消状态。库存预订量应在可售数量内。
聚合边界取决于这些规则。页面上共同展示的数据可以来自多个聚合。数据库外键也只说明存储关系。
设计时列出命令、涉及的状态和校验规则。必须原子维护的规则由同一边界负责。
3. 聚合根
聚合根是外部修改聚合的入口。接口表达业务动作:
order.changeQuantity(productId, quantity);
order.confirm(confirmTime);
order.cancel(reason);
订单根检查状态并更新金额。订单项的修改也经由订单根完成。内部对象可以承担计算,根对象控制修改入口。用户鉴权由应用层完成,业务状态校验留在领域模型中。
3.1 实体与值对象
实体有持续身份,属性变化后仍是同一个对象。订单按订单 ID 识别,状态改变不会创建另一笔订单。
值对象按属性比较,并适合设计为不可变对象。金额由数值与币种共同定义。两份相同金额可以互换,修改时创建新值。
聚合根是拥有身份的实体。聚合内部可以包含其他实体和值对象。仓储按根保存聚合,内部实体无需各自开放写入仓储。
4. 事务边界
应用服务加载聚合,调用业务方法,再用事务保存。提交前,聚合内的规则必须成立。多表存储时,根对象和子对象共同提交。
一个事务修改一个聚合是设计取向。同一数据库可原子修改多个聚合。若每条命令都需要跨聚合事务,应重新检查边界。真正独立的生命周期可用异步流程协作。
跨服务协调的选择见分布式一致性。
5. 聚合之间的引用
聚合引用外部对象时保存 ID 或业务快照:
final class Order {
private CustomerId buyerId;
private List<OrderLine> lines;
}
buyerId 标识购买人,订单项属于订单内部。收货地址可保存下单时快照。当前会员等级由应用层查询会员接口。独立引用能减少对象之间的生命周期耦合。
6. 聚合规模与竞争
聚合过大会增加加载量和写入竞争。无关操作也会争用同一个版本或锁。
例如,客户的全部订单共用一个聚合根。修改两笔不同订单也要争用客户版本。订单有独立生命周期时,应拆成独立聚合。普通只读查询本身并不会争用写锁。
7. 规则分散
若订单项和总额分别独立提交,失败会留下金额差异。二者需要共同维护同一条金额规则。
边界过小的信号包括重复校验和固定的跨对象修改顺序。判断依据是规则是否需要共同成立。表的数量与聚合数量没有固定比例。
8. 并发控制
两个请求可同时通过内存中的规则检查。保存时还要检查原先读取的版本:
UPDATE orders
SET status = ?, version = version + 1
WHERE id = ? AND version = ?;
影响 1 行表示版本匹配。影响 0 行表示记录缺失或版本已经变化。调用方重新读取后区分原因。
子对象的修改也要参与根版本检查。根版本更新和子表更新放进同一事务。热点命令可改用行锁或原子条件更新。
8.1 版本冲突与命令重放
假设两个请求都读取订单版本 7。请求 A 取消订单,提交后版本变成 8。请求 B 确认订单,带版本 7 保存时影响 0 行。
B 应回滚本次事务,并重新加载订单。取消状态会使确认命令校验失败。若只换成版本 8 重发旧对象,就会覆盖合法取消。
可重试命令需要重新计算结果。外部扣款等副作用由业务幂等标识保护。
9. 常见问题
9.1 聚合与表的映射
一个聚合可以映射到多张表。仓储按聚合边界保存对象。报表则可以使用专门的读取模型。
9.2 领域事件与提交时机
内部领域事件可以在事务内处理。跨服务事件应在提交后发布,其可靠交付见Transactional Outbox 与 CDC。
10 · 面试题卡片
10.1 怎样确定聚合边界,聚合过大或过小分别有什么问题
出现公司:暂无已核验的公开来源(本站自测)
题型:综合题
考察重点
- 不变量、聚合根与事务边界。
- 跨聚合协作和最终一致性。
- 大聚合的争用与小聚合的规则泄漏。
参考回答
聚合边界由必须原子维护的不变量决定。
订单项和订单总额需要共同保存。聚合根提供修改入口,事务保证整体提交。跨聚合引用使用 ID 或必要快照。
过大的聚合增加加载量和版本竞争。过小的聚合让同一规则分散到多个提交步骤。独立生命周期有助于识别拆分点。并发写入还需版本条件或锁保护。跨聚合流程按实际一致性要求选择协调方式。
10.2 两个请求通过聚合校验后,版本冲突应该怎样处理?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:根版本、事务回滚与命令重放。
相关内容:版本冲突与命令重放。
参考回答
冲突请求应回滚,并在新状态上重新执行命令。
版本 7 的确认请求遇到版本 8 的取消结果后,应按取消规则拒绝。只替换版本号会覆盖合法状态。根与子对象共同提交,才能保护整个聚合。
10.3 实体、值对象和聚合根有什么区别?
出现公司:暂无已核验的公开来源(本站自测)
考察重点:身份、值相等与聚合修改入口。
相关内容:实体与值对象。
参考回答
实体按身份识别,值对象按属性比较。
订单改状态后仍由原订单 ID 标识。金额则由数值和币种共同定义。聚合根是实体,同时负责聚合的修改入口和规则校验。聚合内可同时包含实体与值对象。