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 |
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。这是事务边界错误处理中最容易踩的坑之一。
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),便于后续分析错误根因。