深入理解事务与数据一致性:从单体ACID到分布式最终一致性
一篇打通事务核心知识、Spring事务原理,以及分布式架构下的数据一致性挑战与解决方案
引言
在软件系统中,事务和数据一致性是保证数据正确、可靠的基石。从单体的银行转账,到微服务的订单与库存协同,每一个涉及数据变更的操作都离不开事务的保护。
然而,随着业务规模扩大,系统从单体演变为分布式架构,传统的事务机制(ACID)面临新的挑战。本文将分为两个部分:
- 第一部分:深入剖析传统事务(ACID特性、Spring事务实现原理、常见失效场景)
- 第二部分:探讨分布式架构下的数据一致性(CAP定理、BASE理论、副本一致性、分布式事务解决方案)
第一部分:传统事务 —— ACID 与 Spring 事务详解
一、事务的四大特性(ACID)
| 特性 | 英文 | 含义 |
|---|---|---|
| 原子性 | Atomicity | 事务中的所有操作要么全部成功,要么全部失败,不可部分完成 |
| 一致性 | Consistency | 事务执行前后,数据必须保持逻辑上的正确性(如转账前后总额不变) |
| 隔离性 | Isolation | 多个事务并发执行时,相互之间不干扰 |
| 持久性 | Durability | 事务一旦提交,其结果永久保存,即使系统崩溃也不丢失 |
数据库通过 undo log 保证原子性,通过 redo log 保证持久性,通过 锁 + MVCC 实现隔离性,而一致性是前三个特性共同保证的最终目标。
二、Spring 事务的三种写法
Spring 提供了三种事务管理方式,灵活度依次递增。
2.1 手动式事务(编程式事务)
最原始的方式,直接使用 PlatformTransactionManager。
@Autowired
private PlatformTransactionManager transactionManager;
public void doSomething() {
// 1. 定义事务属性
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
// 2. 获取事务状态
TransactionStatus status = transactionManager.getTransaction(def);
try {
// 3. 执行业务逻辑
// ...
// 4. 提交事务
transactionManager.commit(status);
} catch (Exception e) {
// 5. 回滚事务
transactionManager.rollback(status);
throw e;
}
}
特点:所见即所得,但代码侵入性强,每个需要事务的地方都要重复编写。
2.2 半自动事务(TransactionTemplate)
Spring 提供的模板类,封装了获取事务、提交/回滚的逻辑,采用回调模式。
@Autowired
private TransactionTemplate transactionTemplate;
public void doSomething() {
transactionTemplate.execute(new TransactionCallback<Void>() {
@Override
public Void doInTransaction(TransactionStatus status) {
try {
// 业务逻辑
// ...
} catch (Exception e) {
status.setRollbackOnly(); // 标记回滚
throw e;
}
return null;
}
});
}
或者在 Java 8 中使用 Lambda:
transactionTemplate.execute(status -> {
// 业务逻辑
return null;
});
特点:减少样板代码,但仍然需要显式调用。查看 TransactionTemplate 源码,其内部正是封装了编程式事务的三步(获取状态 → 执行业务 → 提交/回滚)。
2.3 全自动事务(声明式事务)
通过 @Transactional 注解声明,是 AOP 的典型应用。
@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public void doSomething() {
// 业务逻辑
}
原理:Spring 在 Bean 初始化时,扫描 @Transactional 方法,通过 AOP 动态代理生成代理对象。代理方法中会调用 TransactionInterceptor(事务拦截器),最终调用父类 TransactionAspectSupport.invokeWithinTransaction() 完成事务的开启、提交、回滚。
核心接口 PlatformTransactionManager:
public interface PlatformTransactionManager {
TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
void commit(TransactionStatus status);
void rollback(TransactionStatus status);
}
常见的实现类:
DataSourceTransactionManager:用于单个 JDBC 数据源JpaTransactionManager:用于 JPAJtaTransactionManager:用于分布式事务(JTA)
三、Spring 事务失效的常见场景
使用 @Transactional 时,以下情况会导致事务失效:
| 失效原因 | 说明 | 解决方案 |
|---|---|---|
| 方法不是 public | Spring AOP 只能代理 public 方法 | 改为 public |
| 同类中调用 | 通过 this 调用不会经过代理 | 注入自身或使用 AopContext.currentProxy() |
| 传播类型不支持事务 | 如 Propagation.NOT_SUPPORTED | 检查传播属性 |
| 异常类型不匹配 | 默认只回滚 RuntimeException 和 Error | 指定 rollbackFor = Exception.class |
| 异常被 catch 吞掉 | 方法内 catch 异常后没有重新抛出 | 重新抛出或手动设置回滚 |
| 数据库引擎不支持事务 | 如 MySQL 的 MyISAM | 使用 InnoDB |
四、事务的传播属性(Propagation)
传播属性是 Spring 事务 的概念,用于控制方法嵌套时事务的边界。常用取值:
| 传播行为 | 含义 |
|---|---|
REQUIRED | 当前有事务则加入,否则新建(默认) |
SUPPORTS | 当前有事务则加入,否则以非事务运行 |
MANDATORY | 必须在事务中运行,否则抛异常 |
REQUIRES_NEW | 始终新建事务,挂起当前事务 |
NOT_SUPPORTED | 以非事务运行,挂起当前事务 |
NEVER | 必须在非事务中运行,否则抛异常 |
NESTED | 嵌套事务(Savepoint 机制) |
注意:
REQUIRES_NEW和NESTED容易混淆。REQUIRES_NEW是独立事务,互不干扰;NESTED是内嵌事务,内层回滚不影响外层,但外层回滚会导致内层回滚。
五、Spring 事务与 JDBC 的关系
Spring事务结构图
Spring 事务本质上是对 JDBC 事务的封装。JDBC 原生事务管理:
Connection conn = dataSource.getConnection();
try {
conn.setAutoCommit(false); // 开启事务
// 执行 SQL
conn.commit(); // 提交
} catch (Exception e) {
conn.rollback(); // 回滚
} finally {
conn.close();
}
Spring 通过 DataSourceUtils 管理当前线程的 Connection,同一个线程的多个事务方法共享同一个 Connection,从而实现事务传播。
多数据源场景:通过 AbstractRoutingDataSource 动态路由,在运行时根据规则切换 DataSource,从而获取不同的 Connection。
第二部分:分布式架构下的数据一致性
一、从 ACID 到 CAP / BASE
在单体应用中,我们可以依赖数据库的 ACID 事务保证强一致性。但在分布式系统中,数据被分散到多个节点,传统的本地事务不再适用。
1.1 CAP 定理
2000 年,Eric Brewer 提出 CAP 定理:一个分布式系统最多只能同时满足一致性(Consistency)、可用性(Availability)、分区容错性(Partition tolerance) 中的两个。
- C(一致性):所有节点在同一时间看到相同的数据。
- A(可用性):每个请求都能收到非错的响应,但不保证数据最新。
- P(分区容错性):系统允许网络分区(节点间通信中断)仍能继续运行。
在实际分布式系统中,网络分区无法避免(P 必须满足),因此只能在 C 和 A 之间权衡:
- CP 系统:放弃可用性,保证强一致性(如 ZooKeeper、HBase)
- AP 系统:放弃强一致性,保证最终一致性(如 Cassandra、Eureka)
1.2 BASE 理论
BASE 是对 CAP 中 AP 方案的延伸,核心思想是用最终一致性替代强一致性:
- BA(Basically Available):基本可用,允许部分功能降级或响应延迟。
- S(Soft state):软状态,允许数据存在中间状态(如“订单支付中”)。
- E(Eventually consistent):最终一致性,经过一段时间后数据达到一致。
大多数互联网系统(如电商、社交)选择 AP + 最终一致性,因为高可用比强一致更重要(用户愿意等待几秒看到优惠券同步,但不能接受系统不可用)。
二、副本数据一致性的五种类型
在分布式存储中,副本一致性可以细分为以下级别(由强到弱):
| 等级 | 描述 | 示例 |
|---|---|---|
| 强一致性 | 写操作完成后,任何后续读都能读到最新值 | 主从同步写入(如 Google Spanner) |
| 顺序一致性 | 所有进程看到的操作顺序一致,但不一定实时 | 分布式队列、ZooKeeper 的顺序写 |
| 因果一致性 | 有因果关系的操作有序,无关操作可并发 | 社交网络:先发帖后评论,评论者一定看到帖子 |
| 弱一致性 | 写入后不保证立即读到,但保证最终会读到 | DNS 缓存、CDN |
| 最终一致性 | 弱一致性的特例,保证经过一段时间后数据一致 | 异步复制、消息队列 |
通常说的“最终一致性”是弱一致性的一个子集,强调“最终”会一致,不指定时间上限。
三、分布式事务解决方案
分布式事务的目标是实现跨多个独立数据资源的一致性操作。以下是业界主流方案:
3.1 两阶段提交(2PC)
原理:引入事务协调者(Coordinator),分为准备(Prepare)和提交(Commit)两个阶段。
- 第一阶段(准备) :协调者向所有参与者询问是否可以提交,参与者执行本地事务但不提交,返回“同意”或“取消”。
- 第二阶段(提交) :如果所有参与者都同意,协调者发送 commit;否则发送 rollback。
优点:实现强一致性。
缺点:同步阻塞、单点故障、数据不一致风险(协调者崩溃时部分参与者可能已提交)。
典型实现:XA 协议(MySQL、Oracle 支持)、Atomikos、Seata AT 模式(基于 2PC 变种)。
3.2 三阶段提交(3PC)
为解决 2PC 的阻塞问题,3PC 引入 超时机制 和 预提交阶段:
- CanCommit:询问参与者是否可以提交。
- PreCommit:参与者执行事务但不提交。
- DoCommit:协调者发送 commit。
但 3PC 依然无法完全解决数据不一致问题,且实现复杂,实际应用较少。
3.3 TCC(Try-Confirm-Cancel)
原理:将每个业务操作拆分为三个阶段:
- Try:预留资源(如冻结库存)。
- Confirm:确认执行(扣减冻结库存)。
- Cancel:取消(解冻库存)。
优点:最终一致性、无锁、性能较高。
缺点:业务侵入大,需实现三个接口,处理幂等和空回滚等问题。
典型实现:Hmily、Seata TCC 模式。
3.4 可靠消息最终一致性
原理:通过消息队列保证事务的最终一致性。常用方案:本地消息表 + MQ。
流程:
- 业务方在本地事务中更新业务表,同时插入一条“消息发送”记录。
- 后台定时扫描消息表,将未发送的消息发送到 MQ。
- 消费方接收到消息后执行本地事务,处理完成后确认消费。
- 若消费失败,MQ 重试或进入死信队列人工处理。
优点:解耦、高可用、适合长事务。
缺点:消息可能重复消费,需消费方幂等;最终一致性有时间窗口。
典型实现:RocketMQ 事务消息、RabbitMQ + 本地消息表。
3.5 SAGA 事务
原理:将一个长事务拆分为多个本地事务,每个本地事务有对应的补偿操作(Compensation)。SAGA 由两部分组成:
- 正向操作:Ti 事务
- 补偿操作:Ci 事务(撤销 Ti)
SAGA 有两种协调方式:
- 编排式(Choreography) :参与者通过事件驱动,无需中心协调者。
- 控制式(Orchestration) :SAGA 协调器统一调度。
优点:适用于长流程、微服务架构。
缺点:补偿逻辑复杂、缺乏隔离性(需自行处理脏读)。
典型实现:Seata SAGA 模式、Apache Camel。
四、方案对比与选型建议
| 方案 | 一致性 | 性能 | 业务侵入 | 适用场景 |
|---|---|---|---|---|
| 2PC / XA | 强 | 低(阻塞) | 低 | 金融、银行等对一致性要求极高且节点少的场景 |
| TCC | 最终 | 高 | 高 | 高性能、高并发场景,如支付、扣库存 |
| 可靠消息 | 最终 | 高 | 中 | 跨服务异步解耦,如订单与积分系统 |
| SAGA | 最终 | 中 | 高 | 长事务、微服务流程,如旅游预订、下订单多步骤 |
| 本地事务+重试 | 最终 | 高 | 低 | 单服务多表操作,或允许短暂不一致的业务 |
选型原则:没有银弹。优先考虑能否避免分布式事务(如拆分业务、使用领域事件),如果无法避免,根据业务对一致性、性能、开发成本的容忍度选择合适的方案。
五、分布式架构中的副本一致性(Raft / Paxos)
除了事务一致性,分布式系统还面临副本数据一致性问题(即多个节点上的数据副本如何保持一致)。常见的一致性算法:
- Paxos:Leslie Lamport 提出的经典算法,是分布式一致性理论的基础,但实现复杂。
- Raft:更易理解和实现的共识算法,通过选举 Leader 和日志复制达成一致。
Raft 核心过程:
- 选举 Leader。
- Leader 接收客户端请求,将日志条目复制到所有 Follower。
- 确保多数节点成功写入后,提交日志并响应客户端。
这些算法保证了分布式系统中的 线性一致性(最强的一致性模型),常用于分布式数据库、配置中心等。
总结
| 阶段 | 核心目标 | 关键概念 | 典型技术 |
|---|---|---|---|
| 单体应用 | 强一致性(ACID) | 本地事务、隔离级别 | JDBC、@Transactional |
| 分布式系统 | 最终一致性 / 可用性优先 | CAP、BASE、副本一致性 | Raft、Paxos |
| 跨服务事务 | 解决分布式事务 | 2PC、TCC、消息、SAGA | Seata、RocketMQ |
最后的话:事务和数据一致性是每个后端工程师必须深入理解的主题。从单体到分布式,变化的只是实现手段,不变的是对正确性的追求。希望本文能帮助你在实际项目中做出更合理的设计决策。
参考文档
如果本文对您有帮助,欢迎点赞、收藏、转发,让更多人了解事务与数据一致性的精妙之处。