4. 数据库事务与 ACID 特性:事务隔离级别、乐观锁与悲观锁、分布式事务基础

在领域驱动设计中,聚合(Aggregate)是事务一致性的基本单元。理解数据库事务的 ACID 特性、隔离级别以及并发控制机制,是定义缓存与数据库一致性边界的前提。本章将系统梳理这些核心概念,并建立与 DDD 战术模式的映射关系。

4.1 ACID 特性与 DDD 聚合边界

ACID 是数据库事务的四大基本属性,直接决定了聚合根的设计粒度:

特性 定义 对 DDD 聚合的约束
原子性(Atomicity) 事务中的所有操作要么全部成功,要么全部回滚 一个事务只能修改一个聚合实例,跨聚合操作必须通过领域事件最终一致性
一致性(Consistency) 事务执行前后,数据库始终处于合法状态 聚合的不变规则(Invariant)必须在事务提交前验证通过
隔离性(Isolation) 并发事务之间互不干扰 决定缓存读取策略:读已提交还是可重复读
持久性(Durability) 已提交的事务结果永久保存 缓存写回策略(Write-Behind)必须保证最终持久化
DDD 实践要点: 聚合根的事务边界应严格遵循原子性要求。如果一个业务操作需要修改两个聚合,应拆分为两个独立事务,并通过领域事件或 Saga 模式协调最终一致性,而非依赖分布式事务。

4.2 事务隔离级别与缓存一致性

SQL 标准定义了四种隔离级别,每种级别对缓存与数据库的一致性策略有直接影响:

隔离级别 脏读 不可重复读 幻读 缓存策略建议
读未提交(Read Uncommitted) 可能 可能 可能 不推荐用于业务缓存,数据极易不一致
读已提交(Read Committed) 避免 可能 可能 缓存可接受短时过期,配合 Cache-Aside 模式
可重复读(Repeatable Read) 避免 避免 可能(InnoDB 通过间隙锁避免) 缓存一致性要求高时使用,需注意锁范围
可串行化(Serializable) 避免 避免 避免 性能代价高,仅用于关键数据的一致性校验
常见误区: 很多团队在“读已提交”级别下使用缓存,却期望缓存与数据库完全一致。实际上,读已提交允许不可重复读,缓存中的旧数据可能在事务提交前被其他事务读到。解决方案是:缓存设置合理的 TTL,或使用版本号机制。
事务隔离级别与缓存一致性决策流程 事务隔离级别与缓存一致性决策流程 业务场景分析 一致性 要求高? 读已提交 可重复读 Cache-Aside 版本号 + 缓存 定义一致性边界 隔离级别决定缓存策略,缓存策略反作用于聚合设计

4.3 乐观锁与悲观锁

在 DDD 聚合的并发修改场景中,锁机制是保证数据一致性的核心手段。两种锁策略各有适用场景:

4.3.1 悲观锁(Pessimistic Locking)

  • 原理: 在读取数据时直接加锁,阻止其他事务修改,直到当前事务释放锁。
  • 实现方式: SELECT ... FOR UPDATE(行级锁)。
  • 适用场景: 写冲突频繁、并发修改概率高的聚合(如库存扣减、账户余额操作)。
  • DDD 映射: 适用于聚合根内部状态变更频繁、且必须保证强一致性的场景。

4.3.2 乐观锁(Optimistic Locking)

  • 原理: 不主动加锁,在提交更新时检查数据版本是否被其他事务修改过。
  • 实现方式: 版本号字段(version)或时间戳(last_updated)。
  • 适用场景: 读多写少、冲突概率低的聚合(如用户资料、配置信息)。
  • DDD 映射: 适用于聚合根版本号作为领域事件溯源的基础。
代码示例:乐观锁在 DDD 仓储中的实现
// 聚合根实体
public class Order {
    private Long id;
    private Integer version;  // 乐观锁版本号
    private String status;

    public void updateStatus(String newStatus) {
        // 领域逻辑校验
        if ("SHIPPED".equals(this.status)) {
            throw new IllegalStateException("已发货订单不可修改状态");
        }
        this.status = newStatus;
        this.version++;  // 版本号递增
    }
}

// 仓储实现(JPA 示例)
@Repository
public class OrderRepository {
    @PersistenceContext
    private EntityManager em;

    public void save(Order order) {
        // JPA 自动校验 version,若版本不匹配则抛出 OptimisticLockException
        em.merge(order);
    }
}

4.3.3 锁策略对比

维度 悲观锁 乐观锁
并发性能 低(锁等待阻塞) 高(无锁竞争)
冲突处理 阻塞等待 重试或回滚
适用写冲突 高冲突场景 低冲突场景
数据库支持 SELECT FOR UPDATE 版本号字段
缓存一致性 锁释放后缓存失效 版本号校验后更新缓存
乐观锁与悲观锁在 DDD 聚合中的工作流程 乐观锁与悲观锁在 DDD 聚合中的工作流程 悲观锁流程 1. 读取聚合时加 FOR UPDATE 锁 2. 其他事务等待锁释放 3. 修改聚合后提交事务 4. 释放锁,缓存失效 乐观锁流程 1. 读取聚合(含版本号) 2. 修改聚合,版本号 +1 3. 提交时校验版本号 4. 版本匹配则提交,否则重试 选择依据:写冲突概率 × 性能要求 × 聚合一致性等级

4.4 分布式事务基础

在微服务架构中,一个领域操作可能跨越多个限界上下文(Bounded Context),此时本地数据库事务无法满足需求。分布式事务成为保证跨服务数据一致性的关键手段。

4.4.1 分布式事务的核心挑战

  • 网络不可靠: 服务间调用可能超时、重试或丢失。
  • 数据分区: 每个服务拥有独立的数据库,无法使用全局锁。
  • 一致性模型: 强一致性(CP)与最终一致性(AP)的权衡。

4.4.2 常见分布式事务方案

方案 原理 一致性模型 DDD 适用场景
两阶段提交(2PC) 协调者先询问所有参与者能否提交,再统一提交或回滚 强一致性 跨聚合的强一致性操作(不推荐,性能差)
TCC(Try-Confirm/Cancel) 业务层面预留资源,确认阶段实际执行,取消阶段回滚 最终一致性 需要业务补偿的跨服务操作(如订单+库存)
Saga 模式 将长事务拆分为多个本地事务,每个事务有对应的补偿事务 最终一致性 DDD 推荐的跨聚合协调方式,与领域事件天然契合
事件驱动(Eventual Consistency) 通过消息队列异步传递领域事件,各服务独立处理 最终一致性 DDD 领域事件 + 事件溯源的标准模式
DDD 最佳实践: 优先使用 Saga 模式或事件驱动方式处理跨聚合操作。2PC 虽然能保证强一致性,但会显著降低系统可用性,且与 DDD 的“聚合内强一致、聚合间最终一致”原则相悖。

4.4.3 Saga 模式在 DDD 中的实现要点

  1. 定义 Saga 编排器: 负责协调多个聚合的操作顺序。
  2. 每个步骤对应一个领域命令: 如 CreateOrderCommand、ReserveInventoryCommand。
  3. 每个步骤必须有补偿操作: 如 CancelOrderCommand、ReleaseInventoryCommand。
  4. 使用事件日志记录 Saga 状态: 保证崩溃后可恢复。
避坑指南: 不要将 Saga 的补偿操作设计为“反向操作”的简单取反。例如,订单已发货后的取消操作,不能简单地将状态改回“待支付”,而需要创建退货流程。补偿操作本身也是一个完整的领域流程。

4.5 本章小结

  • ACID 特性决定了聚合的事务边界:一个事务只修改一个聚合。
  • 隔离级别影响缓存策略的选择:读已提交配合 Cache-Aside,可重复读配合版本号缓存。
  • 乐观锁适用于低冲突场景,悲观锁适用于高冲突场景,选择依据是写冲突概率。
  • 分布式事务中,Saga 模式与 DDD 的领域事件机制最为契合,是实现跨聚合最终一致性的推荐方案。