【Spring】事务管理

Spring 事务管理详解

在 Spring 项目开发中,数据库操作常常需要保证一组操作要么全部成功、要么全部失败------这就是事务存在的意义。

Spring 对数据库事务提供了非常友好的封装,让我们可以通过简单的注解就完成事务管理,而不需要手动写大量的 commit / rollback 代码。

本文按"基础概念 → 注解使用 → 隔离级别 → 传播机制"的顺序展开,读完你应该能独立在 Spring 项目中正确配置和使用事务管理


事务的基础概念

事务的操作主要有三步:

  1. 开启事务 START TRANSACTION / BEGIN ------ 在一组操作前开启事务
  2. 提交事务 COMMIT ------ 这组操作全部成功时,提交事务,让数据持久化
  3. 回滚事务 ROLLBACK ------ 这组操作中任何一个出现异常,回滚事务,撤销所有修改

用伪 SQL 来表示就是这样:

sql 复制代码
BEGIN;
    UPDATE account SET balance = balance - 100 WHERE id = 1;  -- A 转出
    UPDATE account SET balance = balance + 100 WHERE id = 2;  -- B 到账
COMMIT;  -- 两条都成功 → 提交
-- 如果任一条失败 → ROLLBACK 回滚

事务的核心特性就是 ACID(原子性、一致性、隔离性、持久性),这里不再展开。


Spring 事务的两种实现方式

Spring 对事务的实现分为两类:

方式 说明 使用场景
编程式事务 手动通过 TransactionTemplatePlatformTransactionManager 控制事务边界 需要精细控制事务的场景,代码侵入性强
声明式事务 通过 @Transactional 注解声明事务,底层基于 AOP 实现 绝大多数业务场景,代码简洁,推荐使用

在项目中,绝大多数情况使用声明式事务就够了。

声明式事务的原理是 Spring AOP:Spring 会为标注了 @Transactional 的 Bean 生成代理对象,在方法执行前后自动开启和提交/回滚事务。

text 复制代码
调用方 → 代理对象(AOP)→ 开启事务 → 执行目标方法 → 提交/回滚事务

如果对 AOP 还不太熟悉,可以参考 \[Spring AOP]。


@Transactional 注解详解

基本用法

@Transactional 注解表示开启了一个事务:

  • 方法正常执行完成 → 事务自动提交
  • 方法抛出异常(ErrorRuntimeException 及其子类) → 事务自动回滚

这里有一个重要的细节:异常被 try-catch 捕获后,事务会正常提交,因为 Spring 感知不到异常。如果需要回滚,有两种做法:

java 复制代码
@Transactional
public void doSomething() {
    try {
        // 业务逻辑
    } catch (Exception e) {
        // 方案1:重新抛出异常,让 Spring 感知并回滚
        throw new RuntimeException(e);

        // 方案2:手动标记回滚(不抛出异常时使用)
        // TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
    }
}
  • 代码位置:Service 层方法上
  • 关键点try-catch 吞掉异常会导致事务不回滚;需要时可用 setRollbackOnly() 手动回滚

作用范围

@Transactional 可以用来修饰方法或类:

  • 修饰方法时 :只有修饰 public 方法时才生效(修饰 private/protected 方法不会报错,但也不生效)------推荐
  • 修饰类时 :对该类中所有的 public 方法都生效

为什么只对 public 方法生效? 因为 Spring 事务基于 AOP 动态代理实现,代理对象只能拦截 public 方法。

rollbackFor 属性

默认情况下,只有 RuntimeExceptionError 会触发回滚。如果希望**受检异常(checked exception)**也触发回滚,需要通过 rollbackFor 指定:

java 复制代码
// 让 IOException 也触发回滚
@Transactional(rollbackFor = {IOException.class})

// 可以指定多个异常类型(数组形式)
@Transactional(rollbackFor = {IOException.class, SQLException.class})
  • 代码位置@Transactional 注解的参数
  • 解决的问题 :默认不回滚受检异常(如 IOException),业务需要时通过 rollbackFor 扩展
  • 注意事项rollbackFor 接受一个 Class 数组,花括号 {} 是数组语法

事务的隔离级别

为什么需要隔离级别

同一个项目中,不同业务对数据一致性的要求不同。比如:

  • 财务报表要求读到绝对准确的数据,不能出现脏读
  • 商品列表页面可以容忍少量不一致,追求更高并发性能

Spring 允许我们手动设置事务隔离级别,在一致性和性能之间做取舍。

SQL 标准隔离级别(回顾)

隔离级别 脏读 不可重复读 幻读
READ UNCOMMITTED(读未提交)
READ COMMITTED(读已提交)
REPEATABLE READ(可重复读)
SERIALIZABLE(串行化)

MySQL 默认隔离级别是 REPEATABLE READ(可重复读)。

Spring 中的 5 种隔离级别

java 复制代码
@Transactional(isolation = Isolation.READ_COMMITTED)
Spring 常量 对应 SQL 标准 说明
Isolation.DEFAULT --- 以数据库默认隔离级别为准(最常用
Isolation.READ_UNCOMMITTED READ UNCOMMITTED 读未提交
Isolation.READ_COMMITTED READ COMMITTED 读已提交,解决脏读
Isolation.REPEATABLE_READ REPEATABLE READ 可重复读,MySQL 默认级别
Isolation.SERIALIZABLE SERIALIZABLE 串行化,最高隔离级别
  • 代码位置@Transactional 注解的 isolation 属性
  • 关键点 :大多数情况用 DEFAULT 即可,让数据库自己决定
  • 注意事项:隔离级别越高,数据一致性越好,但并发性能越差

事务的传播机制

什么是事务传播

当一个 Service 方法调用另一个 Service 方法,且两者都有事务时,就产生了事务传播------被调用方法的事务如何处理与调用方事务的关系?

java 复制代码
@Service
public class AService {

    @Autowired
    private BService bService;

    @Transactional
    public void order() {
        // A 的事务中调用了 B 的方法
        bService.doSomething();  // B 也有事务 → 产生传播
        // 其他业务逻辑
    }
}

三种基本形态

先理解三种最基本的事务关系:

形态 说明 回滚影响
融入 A 和 B 是同一个事务 A 回滚 → B 回滚,B 回滚 → A 回滚
挂起 A 和 B 是两个独立的事务 A 回滚 → B 不回滚,互不影响
嵌套 B 的事务嵌套在 A 的事务里 B 回滚 → A 不回滚(局部回滚),A 回滚 → B 回滚

用伪 SQL 来直观理解:

融入(同一个事务)

sql 复制代码
BEGIN;
    UPDATE yyyy;
    UPDATE xxxx;   -- B 的操作,共用 A 的事务
    UPDATE zzzz;
COMMIT;

挂起(两个独立事务)

sql 复制代码
BEGIN;              -- A 的事务
    UPDATE yyyy;
-- A 的事务挂起,保存状态 --
BEGIN;              -- B 的事务(独立)
    UPDATE xxxx;
COMMIT;             -- B 提交
-- A 的事务恢复 --
    UPDATE zzzz;
COMMIT;             -- A 提交

嵌套(B 嵌套在 A 里)

sql 复制代码
BEGIN;              -- A 的事务
    UPDATE yyyy;
    SAVEPOINT sp;   -- 保存点
        UPDATE xxxx; -- B 的操作
    -- B 失败 → ROLLBACK TO sp(局部回滚,A 不受影响)--
    UPDATE zzzz;
COMMIT;

Spring 的 7 种传播行为

传播行为 A 有事务 A 无事务 回滚特点
REQUIRED(默认) B 融入 A B 新建事务 任一失败,全部回滚
SUPPORTS B 融入 A B 以非事务方式运行 有事务时随 A 回滚,无事务时不回滚
MANDATORY B 融入 A 抛出异常 强制要求调用方有事务
REQUIRES_NEW A 挂起,B 新建独立事务 B 新建事务 A 和 B 互不影响
NOT_SUPPORTED A 挂起,B 以非事务运行 B 以非事务运行 B 不回滚
NEVER 抛出异常 B 以非事务运行 不允许在有事务的环境运行
NESTED B 嵌套在 A 的事务中 B 新建事务 B 回滚不影响 A(局部回滚)

重点:NESTED vs REQUIRED

这两个容易混淆,关键区别在于局部回滚

  • REQUIRED :B 和 A 是同一个事务,任何一方失败 → 全部回滚
  • NESTED :B 嵌套执行,B 失败 → 只有 B 的部分回滚,A 不受影响

嵌套事务之所以能实现局部回滚,是因为数据库的**保存点(Savepoint)**机制。Spring 利用保存点,在嵌套事务失败时只回滚到保存点位置。

设置传播行为

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public void doSomething() { ... }

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void doIndependent() { ... }

@Transactional(propagation = Propagation.NESTED)
public void doNested() { ... }

手动回滚当前事务

无论哪种传播机制,都可以在代码中手动标记回滚:

java 复制代码
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
  • 代码位置:事务方法内部
  • 解决的问题:在不抛出异常的情况下回滚事务(如业务校验不通过)
  • 注意事项 :这只回滚当前事务 ;如果是 NESTED,只回滚嵌套事务部分

常见问题与注意事项

@Transactional 不生效?

排查清单:

  1. 方法是不是 public ------ 非 public 方法不生效
  2. 是不是同类内部调用? ------ 同类中方法 A 直接调用方法 B,B 的 @Transactional 不走代理,不生效
  3. 异常是不是被 try-catch 吃掉了? ------ Spring 感知不到异常就不会回滚
  4. 数据库引擎是否支持事务? ------ 比如 MySQL 的 MyISAM 引擎不支持事务

什么时候用 REQUIRES_NEW?

典型场景:日志记录。即使业务操作失败回滚,日志记录也应该独立提交,不受主事务回滚的影响。

大多数情况怎么选?

  • 隔离级别 :用 DEFAULT,交给数据库
  • 传播行为 :用 REQUIRED(默认),能满足 90% 的场景
  • 没有明确需求时不要去改默认值

总结

Spring 事务管理的核心就四个点:

  1. @Transactional 注解 :声明式事务,基于 AOP 实现,作用在 public 方法上
  2. 回滚规则 :默认 RuntimeExceptionError 回滚,try-catch 捕获后不回滚(除非手动标记)
  3. 隔离级别 :5 种,大多数情况用 DEFAULT
  4. 传播机制 :7 种,默认 REQUIRED(融入式),需要独立事务用 REQUIRES_NEW,需要局部回滚用 NESTED

掌握这些,日常开发中的事务场景基本都能应对。