4. 聚合(Aggregate)设计:聚合根的选择、聚合边界划分、事务一致性与最终一致性权衡
聚合是领域驱动设计中用于封装业务不变条件(Invariant)的核心模式。它将一组具有紧密关联、必须保持数据一致性的实体和值对象组合为一个整体,通过聚合根(Aggregate Root)作为外部访问的唯一入口。错误的聚合设计会导致事务范围过大、性能瓶颈或数据不一致。本节将系统讲解聚合根的选择原则、边界划分方法以及一致性策略的权衡。
4.1 聚合根的选择原则
聚合根是聚合中唯一能被外部引用的实体,负责维护聚合内部的不变条件。选择聚合根需遵循以下原则:
| 原则 | 说明 | 反例 |
|---|---|---|
| 全局唯一标识 | 聚合根必须拥有全局唯一的 ID,外部通过 ID 引用聚合,而非对象引用 | 将值对象或子实体直接暴露给外部修改 |
| 业务不变条件守护者 | 聚合根负责校验和保证聚合内所有业务规则不被破坏 | 允许外部直接修改子实体的状态而不经过聚合根 |
| 事务边界 | 一个事务只应修改一个聚合实例,聚合根是事务提交的决策点 | 在一个事务中同时修改多个聚合实例 |
| 生命周期一致性 | 聚合内对象的创建、修改、删除必须由聚合根统一管理 | 子实体独立于聚合根被持久化或删除 |
实战经验:在审批流系统中,审批单(ApprovalOrder)通常是聚合根,而审批节点(ApprovalNode)、审批记录(ApprovalRecord)是聚合内的实体或值对象。外部系统只能通过审批单 ID 发起审批、驳回或转办操作,不能直接操作某个审批节点。
4.2 聚合边界划分方法
聚合边界划分的核心是识别哪些对象必须保持强一致性,哪些可以接受最终一致性。以下是三种常用的边界划分策略:
- 基于不变条件(Invariant):列出所有业务规则,将必须同时满足的规则所涉及的对象划入同一聚合。例如“订单总金额必须等于所有订单行金额之和”,则订单与订单行必须在同一聚合内。
- 基于事务范围:分析业务操作中需要原子性提交的数据变更范围。如果一个操作需要同时更新 A 和 B 才能保证业务正确,则 A 和 B 应属于同一聚合。
- 基于引用频率:如果两个实体在绝大多数业务场景中总是同时被加载和修改,则考虑合并为一个聚合;如果它们经常被独立访问,则应拆分为不同聚合。
边界划分避坑:不要将“数据关系紧密”等同于“必须放在同一聚合”。例如,用户与用户地址虽然关系紧密,但地址变更通常不需要与用户信息变更保持事务一致性,因此应拆分为两个聚合。
下图展示了审批流系统中聚合边界的典型划分:
4.3 事务一致性与最终一致性权衡
聚合设计中最关键的决策是确定哪些操作需要强事务一致性,哪些可以接受最终一致性。错误的权衡会导致系统性能下降或数据错误。
| 维度 | 事务一致性(强一致性) | 最终一致性 |
|---|---|---|
| 适用场景 | 聚合内部操作、业务不变条件保护 | 跨聚合操作、跨限界上下文协作 |
| 实现方式 | 本地事务(数据库 ACID) | 事件驱动、Saga、TCC、消息队列 |
| 性能影响 | 高(锁范围大、并发低) | 低(异步、无锁或局部锁) |
| 数据一致性保证 | 立即一致,无中间状态 | 最终一致,存在短暂不一致窗口 |
| 典型例子 | 审批单提交时校验所有节点完整性 | 审批通过后异步发送通知、更新统计 |
常见错误:将跨聚合的业务流程放在一个数据库事务中实现。例如,在审批通过的事务中同时更新审批单状态、发送通知、更新用户积分。这会导致事务范围过大、锁冲突频繁,且通知失败会导致整个审批回滚。正确的做法是:审批单状态变更使用本地事务,通知和积分更新通过领域事件异步处理。
4.4 一致性策略选择决策树
在实际建模中,可通过以下决策树快速判断应采用哪种一致性策略:
4.5 实战案例:审批流中的一致性设计
以审批流中“审批通过”操作为例,展示一致性策略的具体应用:
场景:审批人点击“通过”按钮,系统需要:
1. 更新当前审批节点状态为“已通过”
2. 判断是否所有节点都已通过,若是则更新审批单状态为“已完成”
3. 发送通知给下一审批人或发起人
4. 更新审批单的统计信息(如平均审批时长)
一致性决策:
· 操作 1 和 2 涉及同一聚合(审批单)内的多个实体,使用本地事务强一致性。
· 操作 3 和 4 属于跨聚合操作(通知聚合、统计聚合),通过领域事件实现最终一致性。
1. 更新当前审批节点状态为“已通过”
2. 判断是否所有节点都已通过,若是则更新审批单状态为“已完成”
3. 发送通知给下一审批人或发起人
4. 更新审批单的统计信息(如平均审批时长)
一致性决策:
· 操作 1 和 2 涉及同一聚合(审批单)内的多个实体,使用本地事务强一致性。
· 操作 3 和 4 属于跨聚合操作(通知聚合、统计聚合),通过领域事件实现最终一致性。
// 聚合根:ApprovalOrder
public class ApprovalOrder extends AbstractAggregateRoot<ApprovalOrderId> {
private List<ApprovalNode> nodes;
private OrderStatus status;
public void approve(String nodeId, String userId) {
// 1. 校验当前节点是否可审批(不变条件)
ApprovalNode currentNode = findCurrentNode();
if (!currentNode.canApprove(userId)) {
throw new BusinessException("当前用户无权审批此节点");
}
// 2. 更新节点状态(聚合内强一致性)
currentNode.setStatus(NodeStatus.APPROVED);
currentNode.setApprover(userId);
currentNode.setApprovedAt(LocalDateTime.now());
// 3. 判断是否所有节点已完成
if (allNodesCompleted()) {
this.status = OrderStatus.COMPLETED;
}
// 4. 发布领域事件(跨聚合最终一致性)
registerEvent(new ApprovalCompletedEvent(this.id, nodeId, userId));
}
}
// 事件处理器(异步)
@EventListener
public void handleApprovalCompleted(ApprovalCompletedEvent event) {
// 发送通知(最终一致性)
notificationService.sendApprovalNotification(event.getOrderId());
// 更新统计(最终一致性)
statisticsService.updateApprovalMetrics(event.getOrderId());
}
4.6 聚合设计常见陷阱
- 巨型聚合(God Aggregate):将过多对象放入同一聚合,导致事务范围过大、并发冲突严重。应遵循“小聚合”原则,只包含必须强一致的对象。
- 贫血聚合根:聚合根只作为数据容器,不包含业务逻辑。应确保聚合根包含所有与不变条件相关的业务方法。
- 跨聚合对象引用:在聚合内直接持有其他聚合的对象引用,导致边界模糊。应只通过 ID 引用其他聚合。
- 忽视最终一致性窗口:在跨聚合场景中未考虑短暂的不一致状态,导致业务判断错误。应在业务层面设计补偿或重试机制。
设计检查清单:
✓ 每个聚合是否只有一个聚合根?
✓ 聚合根是否通过 ID 被外部引用?
✓ 聚合内所有对象是否必须同时满足不变条件?
✓ 跨聚合操作是否都采用了最终一致性方案?
✓ 是否设计了补偿机制处理最终一致性失败场景?
✓ 每个聚合是否只有一个聚合根?
✓ 聚合根是否通过 ID 被外部引用?
✓ 聚合内所有对象是否必须同时满足不变条件?
✓ 跨聚合操作是否都采用了最终一致性方案?
✓ 是否设计了补偿机制处理最终一致性失败场景?