4. 聚合(Aggregate)设计:聚合根的选择、聚合边界划分、事务一致性与最终一致性权衡

聚合是领域驱动设计中用于封装业务不变条件(Invariant)的核心模式。它将一组具有紧密关联、必须保持数据一致性的实体和值对象组合为一个整体,通过聚合根(Aggregate Root)作为外部访问的唯一入口。错误的聚合设计会导致事务范围过大、性能瓶颈或数据不一致。本节将系统讲解聚合根的选择原则、边界划分方法以及一致性策略的权衡。

4.1 聚合根的选择原则

聚合根是聚合中唯一能被外部引用的实体,负责维护聚合内部的不变条件。选择聚合根需遵循以下原则:

原则 说明 反例
全局唯一标识 聚合根必须拥有全局唯一的 ID,外部通过 ID 引用聚合,而非对象引用 将值对象或子实体直接暴露给外部修改
业务不变条件守护者 聚合根负责校验和保证聚合内所有业务规则不被破坏 允许外部直接修改子实体的状态而不经过聚合根
事务边界 一个事务只应修改一个聚合实例,聚合根是事务提交的决策点 在一个事务中同时修改多个聚合实例
生命周期一致性 聚合内对象的创建、修改、删除必须由聚合根统一管理 子实体独立于聚合根被持久化或删除
实战经验:在审批流系统中,审批单(ApprovalOrder)通常是聚合根,而审批节点(ApprovalNode)、审批记录(ApprovalRecord)是聚合内的实体或值对象。外部系统只能通过审批单 ID 发起审批、驳回或转办操作,不能直接操作某个审批节点。

4.2 聚合边界划分方法

聚合边界划分的核心是识别哪些对象必须保持强一致性,哪些可以接受最终一致性。以下是三种常用的边界划分策略:

  1. 基于不变条件(Invariant):列出所有业务规则,将必须同时满足的规则所涉及的对象划入同一聚合。例如“订单总金额必须等于所有订单行金额之和”,则订单与订单行必须在同一聚合内。
  2. 基于事务范围:分析业务操作中需要原子性提交的数据变更范围。如果一个操作需要同时更新 A 和 B 才能保证业务正确,则 A 和 B 应属于同一聚合。
  3. 基于引用频率:如果两个实体在绝大多数业务场景中总是同时被加载和修改,则考虑合并为一个聚合;如果它们经常被独立访问,则应拆分为不同聚合。
边界划分避坑:不要将“数据关系紧密”等同于“必须放在同一聚合”。例如,用户与用户地址虽然关系紧密,但地址变更通常不需要与用户信息变更保持事务一致性,因此应拆分为两个聚合。

下图展示了审批流系统中聚合边界的典型划分:

审批流系统聚合边界划分示例 聚合:审批单 聚合根:ApprovalOrder ├─ ApprovalNode(实体) ├─ ApprovalRecord(实体) ├─ FormData(值对象) └─ Attachment(值对象) 事务边界:强一致性 不变条件: · 审批节点顺序不可跳跃 · 同一节点不可重复审批 聚合:用户 聚合根:User ├─ UserRole(值对象) └─ Department(值对象) 聚合:通知 聚合根:Notification ├─ NotificationChannel └─ Template(值对象) 引用 UserId 引用 ApprovalOrderId 说明:聚合之间通过全局唯一 ID 引用,不持有对象引用,保证边界清晰

4.3 事务一致性与最终一致性权衡

聚合设计中最关键的决策是确定哪些操作需要强事务一致性,哪些可以接受最终一致性。错误的权衡会导致系统性能下降或数据错误。

维度 事务一致性(强一致性) 最终一致性
适用场景 聚合内部操作、业务不变条件保护 跨聚合操作、跨限界上下文协作
实现方式 本地事务(数据库 ACID) 事件驱动、Saga、TCC、消息队列
性能影响 高(锁范围大、并发低) 低(异步、无锁或局部锁)
数据一致性保证 立即一致,无中间状态 最终一致,存在短暂不一致窗口
典型例子 审批单提交时校验所有节点完整性 审批通过后异步发送通知、更新统计
常见错误:将跨聚合的业务流程放在一个数据库事务中实现。例如,在审批通过的事务中同时更新审批单状态、发送通知、更新用户积分。这会导致事务范围过大、锁冲突频繁,且通知失败会导致整个审批回滚。正确的做法是:审批单状态变更使用本地事务,通知和积分更新通过领域事件异步处理。

4.4 一致性策略选择决策树

在实际建模中,可通过以下决策树快速判断应采用哪种一致性策略:

一致性策略选择决策树 操作涉及多个对象? 否:单对象操作 使用本地事务(强一致性) 是:多对象操作 是否在同一聚合内? 是:聚合内强一致性 否:跨聚合最终一致性 采用事件驱动 / Saga / TCC 通过领域事件异步协调 核心原则:聚合内强一致性,聚合间最终一致性

4.5 实战案例:审批流中的一致性设计

以审批流中“审批通过”操作为例,展示一致性策略的具体应用:

场景:审批人点击“通过”按钮,系统需要:
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 被外部引用?
✓ 聚合内所有对象是否必须同时满足不变条件?
✓ 跨聚合操作是否都采用了最终一致性方案?
✓ 是否设计了补偿机制处理最终一致性失败场景?