事务与数据一致性

0 阅读11分钟

深入理解事务与数据一致性:从单体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:用于 JPA
  • JtaTransactionManager:用于分布式事务(JTA)

三、Spring 事务失效的常见场景

使用 @Transactional 时,以下情况会导致事务失效:

失效原因说明解决方案
方法不是 publicSpring AOP 只能代理 public 方法改为 public
同类中调用通过 this 调用不会经过代理注入自身或使用 AopContext.currentProxy()
传播类型不支持事务Propagation.NOT_SUPPORTED检查传播属性
异常类型不匹配默认只回滚 RuntimeExceptionError指定 rollbackFor = Exception.class
异常被 catch 吞掉方法内 catch 异常后没有重新抛出重新抛出或手动设置回滚
数据库引擎不支持事务如 MySQL 的 MyISAM使用 InnoDB

四、事务的传播属性(Propagation)

传播属性是 Spring 事务 的概念,用于控制方法嵌套时事务的边界。常用取值:

传播行为含义
REQUIRED当前有事务则加入,否则新建(默认)
SUPPORTS当前有事务则加入,否则以非事务运行
MANDATORY必须在事务中运行,否则抛异常
REQUIRES_NEW始终新建事务,挂起当前事务
NOT_SUPPORTED以非事务运行,挂起当前事务
NEVER必须在非事务中运行,否则抛异常
NESTED嵌套事务(Savepoint 机制)

注意REQUIRES_NEWNESTED 容易混淆。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 引入 超时机制预提交阶段

  1. CanCommit:询问参与者是否可以提交。
  2. PreCommit:参与者执行事务但不提交。
  3. DoCommit:协调者发送 commit。

但 3PC 依然无法完全解决数据不一致问题,且实现复杂,实际应用较少。

3.3 TCC(Try-Confirm-Cancel)

原理:将每个业务操作拆分为三个阶段:

  • Try:预留资源(如冻结库存)。
  • Confirm:确认执行(扣减冻结库存)。
  • Cancel:取消(解冻库存)。

优点:最终一致性、无锁、性能较高。
缺点:业务侵入大,需实现三个接口,处理幂等和空回滚等问题。

典型实现:Hmily、Seata TCC 模式。

3.4 可靠消息最终一致性

原理:通过消息队列保证事务的最终一致性。常用方案:本地消息表 + MQ。

流程

  1. 业务方在本地事务中更新业务表,同时插入一条“消息发送”记录。
  2. 后台定时扫描消息表,将未发送的消息发送到 MQ。
  3. 消费方接收到消息后执行本地事务,处理完成后确认消费。
  4. 若消费失败,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 核心过程

  1. 选举 Leader。
  2. Leader 接收客户端请求,将日志条目复制到所有 Follower。
  3. 确保多数节点成功写入后,提交日志并响应客户端。

这些算法保证了分布式系统中的 线性一致性(最强的一致性模型),常用于分布式数据库、配置中心等。


总结

阶段核心目标关键概念典型技术
单体应用强一致性(ACID)本地事务、隔离级别JDBC、@Transactional
分布式系统最终一致性 / 可用性优先CAP、BASE、副本一致性Raft、Paxos
跨服务事务解决分布式事务2PC、TCC、消息、SAGASeata、RocketMQ

最后的话:事务和数据一致性是每个后端工程师必须深入理解的主题。从单体到分布式,变化的只是实现手段,不变的是对正确性的追求。希望本文能帮助你在实际项目中做出更合理的设计决策。


参考文档

  1. Spring Framework 事务源码
  2. CAP 定理原始论文
  3. BASE 论文
  4. Raft 一致性算法
  5. Seata 分布式事务解决方案

如果本文对您有帮助,欢迎点赞、收藏、转发,让更多人了解事务与数据一致性的精妙之处。