3. 事务边界基础:ACID特性、本地事务与分布式事务、事务的传播行为

事务是保证数据一致性的核心机制。在微服务与分布式系统中,事务边界从单一的数据库连接扩展到了跨服务、跨资源的调用链。理解事务的ACID特性、本地与分布式事务的区别,以及事务传播行为,是正确设计错误处理与可观测性策略的前提。

3.1 ACID特性:事务的四大基石

ACID是数据库事务正确执行的四个基本特性,也是衡量事务边界是否完整的关键标准。

特性 全称 核心含义 违反后果
原子性 Atomicity 事务中的所有操作要么全部成功,要么全部回滚,不存在中间状态。 部分更新导致数据不一致
一致性 Consistency 事务执行前后,数据库必须满足所有预定义的约束(如唯一键、外键)。 脏数据、约束破坏
隔离性 Isolation 并发执行的事务之间互不干扰,每个事务都感觉自己是唯一运行的。 脏读、不可重复读、幻读
持久性 Durability 一旦事务提交,其结果就是永久的,即使系统崩溃也不会丢失。 数据丢失
可观测性视角: 在OpenTelemetry调用链中,事务的ACID特性可以通过Span的status属性(OK/ERROR)和事件(Event)来追踪。原子性失败通常表现为Span状态为ERROR,并伴随回滚事件。

3.2 本地事务与分布式事务

根据事务边界跨越的资源范围,事务分为本地事务和分布式事务。两者的实现难度、性能开销和可观测性挑战截然不同。

3.2.1 本地事务

本地事务作用于单一数据库或单一资源管理器(如单个MySQL实例、单个Redis)。它由数据库自身的事务管理器直接控制,通常通过BEGIN、COMMIT、ROLLBACK语句实现。

-- 本地事务示例(MySQL)
BEGIN;
UPDATE account SET balance = balance - 100 WHERE user_id = 1;
UPDATE account SET balance = balance + 100 WHERE user_id = 2;
COMMIT;
-- 如果任何一步失败,执行 ROLLBACK

3.2.2 分布式事务

分布式事务涉及多个独立的资源管理器(如多个数据库、消息队列、缓存服务),需要协调者(Coordinator)来保证所有参与者的原子性提交或回滚。

常见的分布式事务模型包括:

  • XA协议(两阶段提交,2PC): 准备阶段(Prepare) + 提交阶段(Commit)。强一致性,但性能差,存在协调者单点阻塞风险。
  • TCC(Try-Confirm-Cancel): 业务层面的补偿型事务。Try阶段预留资源,Confirm阶段确认执行,Cancel阶段回滚预留资源。
  • Saga模式: 将长事务拆分为多个本地事务,每个本地事务都有对应的补偿操作。通过事件驱动或编排器执行。
  • 最终一致性(消息表/本地消息表): 利用消息队列异步同步状态,允许短暂的不一致,最终达到一致。
关键对比:
维度 本地事务 分布式事务
资源范围 单一资源 多资源/多服务
一致性模型 强一致性(ACID) 强一致性(2PC)或最终一致性(Saga/TCC)
性能开销 低 高(网络通信、锁等待)
可观测性复杂度 低(单Span即可追踪) 高(需要跨服务追踪多个Span)
典型实现 Spring @Transactional Seata、TCC-Transaction、Saga
本地事务 vs 分布式事务 结构对比 本地事务 单一资源管理器 BEGIN UPDATE / INSERT COMMIT / ROLLBACK 分布式事务 多资源协调 协调者(Coordinator) 资源A 资源B Prepare → Commit / Rollback

3.3 事务的传播行为

事务传播行为定义了当多个事务性方法相互调用时,事务边界如何合并或隔离。这是Spring框架中@Transactional注解的核心配置之一,也是理解错误处理中事务嵌套的关键。

3.3.1 七种传播行为详解

传播行为 枚举值 行为描述 典型场景
REQUIRED Propagation.REQUIRED 支持当前事务;如果不存在,则新建一个。这是默认行为。 大多数业务方法,希望整个调用链在同一个事务中。
REQUIRES_NEW Propagation.REQUIRES_NEW 总是新建一个事务;如果当前存在事务,则挂起当前事务。 日志记录、审计操作,不希望被主事务回滚影响。
NESTED Propagation.NESTED 如果当前存在事务,则在嵌套事务内执行;否则行为同REQUIRED。 需要部分回滚的复杂业务逻辑(使用保存点)。
SUPPORTS Propagation.SUPPORTS 支持当前事务;如果不存在,则以非事务方式执行。 查询方法,有事务则参与,无事务也不强求。
NOT_SUPPORTED Propagation.NOT_SUPPORTED 以非事务方式执行;如果当前存在事务,则挂起当前事务。 执行耗时操作,避免占用数据库连接。
MANDATORY Propagation.MANDATORY 必须在一个现有的事务中执行;否则抛出异常。 强制调用方必须开启事务的方法。
NEVER Propagation.NEVER 必须以非事务方式执行;如果当前存在事务,则抛出异常。 禁止在事务中执行的操作(如发送外部通知)。

3.3.2 REQUIRED vs REQUIRES_NEW 核心差异

这两个传播行为在实际开发中最常用,也最容易引发错误处理问题。它们的核心区别在于事务边界是否独立。

  • REQUIRED(默认): 所有方法共享同一个事务。内部方法抛出异常会导致整个事务回滚,即使外部方法捕获了异常。
  • REQUIRES_NEW: 内部方法拥有独立的事务。内部方法回滚不会影响外部事务;外部方法回滚也不会影响已经提交的内部事务。
常见陷阱: 使用REQUIRED时,内部方法的异常即使被try-catch捕获,如果异常类型匹配回滚规则(默认是RuntimeException),事务仍然会被标记为rollback-only,导致外部方法提交时抛出UnexpectedRollbackException。这是事务边界错误处理中最容易踩的坑之一。
REQUIRED vs REQUIRES_NEW 事务边界对比 REQUIRED(共享事务) 外部事务 T1 外部方法:操作A (成功) 内部方法:操作B (抛出异常) 整个T1回滚 REQUIRES_NEW(独立事务) 外部事务 T1 外部方法:操作A (成功) 内部事务 T2:操作B (独立提交/回滚) 外部T1正常提交 REQUIRED:内部异常导致整个事务回滚;REQUIRES_NEW:内部事务独立,外部不受影响

3.4 传播行为与错误处理的关联

在OpenTelemetry调用链中,事务传播行为直接影响Span的父子关系和错误传播路径:

  • REQUIRED: 所有方法在同一个Span(或同一个Trace下的连续Span)中执行。内部方法的错误会冒泡到外部,导致整个Trace被标记为ERROR。
  • REQUIRES_NEW: 内部方法会创建一个独立的Span(甚至可能是一个独立的Trace,取决于上下文传播配置)。内部错误不会污染外部Span的状态。
  • NESTED: 在支持保存点的数据库中,内部回滚可以只回滚到保存点,外部事务仍可继续提交。这在调用链中表现为部分Span回滚,但整体Trace可能仍为OK。
实践建议: 在设计错误处理策略时,应根据业务需求选择传播行为。对于不可分割的核心操作(如资金转账),使用REQUIRED保证原子性;对于可独立失败的辅助操作(如发送通知、记录日志),使用REQUIRES_NEW隔离失败影响。同时,在OpenTelemetry中为每个事务边界添加明确的span.attribute(如transaction.propagation),便于后续分析错误根因。