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,或使用版本号机制。
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 | 版本号字段 |
| 缓存一致性 | 锁释放后缓存失效 | 版本号校验后更新缓存 |
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 中的实现要点
- 定义 Saga 编排器: 负责协调多个聚合的操作顺序。
- 每个步骤对应一个领域命令: 如
CreateOrderCommand、ReserveInventoryCommand。 - 每个步骤必须有补偿操作: 如
CancelOrderCommand、ReleaseInventoryCommand。 - 使用事件日志记录 Saga 状态: 保证崩溃后可恢复。
避坑指南: 不要将 Saga 的补偿操作设计为“反向操作”的简单取反。例如,订单已发货后的取消操作,不能简单地将状态改回“待支付”,而需要创建退货流程。补偿操作本身也是一个完整的领域流程。
4.5 本章小结
- ACID 特性决定了聚合的事务边界:一个事务只修改一个聚合。
- 隔离级别影响缓存策略的选择:读已提交配合 Cache-Aside,可重复读配合版本号缓存。
- 乐观锁适用于低冲突场景,悲观锁适用于高冲突场景,选择依据是写冲突概率。
- 分布式事务中,Saga 模式与 DDD 的领域事件机制最为契合,是实现跨聚合最终一致性的推荐方案。