Spring事务

注意
本文最后更新于 2025-05-07,文中内容可能已过时。

事务的核心概念(传播行为、隔离级别)、底层实现原理,掌握事务失效场景、回滚规则和配置方式。

Spring 事务依赖 数据库事务(底层是数据库的事务支持,如 MySQL 的 InnoDB 引擎),若数据库不支持事务,Spring 事务无效。

一、事务核心:传播行为(7种)+ 隔离级别(4种)

事务的两大核心属性,决定了“事务之间如何交互”(传播行为)和“事务之间的隔离程度”(隔离级别),面试必背,重点突破传播行为的3种常用类型。

1. 事务传播行为

定义:当一个事务方法调用另一个事务方法时,如何决定事务的边界(新事务是否创建、当前事务是否参与),核心是“嵌套事务的行为规则”。

传播行为类型 核心含义(面试通俗答法) 适用场景 是否新建事务
REQUIRED(默认) 如果当前有事务,就加入当前事务;如果没有,就新建一个事务。 绝大多数业务场景(如新增用户+新增用户日志) 当前无事务则新建,有则加入
REQUIRES_NEW 无论当前是否有事务,都新建一个独立事务;原事务暂停,新事务执行完后,原事务继续。 需要独立提交/回滚的场景(如记录操作日志,即使主事务回滚,日志也要保留) 必新建独立事务,与原事务隔离
NESTED 如果当前有事务,就在当前事务内新建一个“嵌套事务”(子事务);子事务依赖父事务,父事务回滚子事务也回滚,子事务回滚不影响父事务。 需要部分回滚的场景(如批量新增,其中一条失败,仅回滚该条,其余成功) 不新建独立事务,嵌套在当前事务内
SUPPORTS 如果当前有事务,就加入;没有,就以非事务方式执行(不开启事务)。 可选事务场景(如查询接口,有事务则参与,无则不开启) 不主动新建
MANDATORY 必须在当前有事务的情况下执行,否则抛出异常。 强制要求事务的场景(如核心转账操作) 不新建,无事务则抛异常
NOT_SUPPORTED 无论当前是否有事务,都以非事务方式执行;若当前有事务,暂停原事务。 无需事务的场景(如纯查询,避免事务占用资源) 不新建,有事务则暂停
NEVER 必须在无事务的情况下执行,否则抛出异常。 严禁事务的场景(如日志清理,避免锁表) 不新建,有事务则抛异常

2. 重点:REQUIRED / REQUIRES_NEW / NESTED 区别 🔴

核心区分:事务独立性、回滚影响,用“嵌套场景示例”理解。

对比维度 REQUIRED(默认) REQUIRES_NEW NESTED
事务独立性 与父事务共用一个事务,无独立性 与父事务完全独立,两个事务互不影响 子事务依赖父事务,无独立事务,属于父事务的一部分
回滚影响 父/子事务一方回滚,另一方也会跟着回滚(同属一个事务) 父事务回滚,不影响子事务(子事务已独立提交);子事务回滚,不影响父事务 父事务回滚,子事务必回滚;子事务回滚,父事务可选择继续执行(仅回滚子部分)
嵌套场景示例(父方法A调用子方法B) A和B共用一个事务,B抛异常回滚,A也回滚 A开启事务,调用B时B新建独立事务,B提交后,A抛异常回滚,B的结果不回滚 A开启事务,B嵌套在A中,B抛异常回滚,A可捕获异常继续执行,仅B的操作回滚

3. 事务隔离级别(4种,对应数据库隔离级别)

定义:解决“并发事务之间的干扰问题”,隔离级别越高,并发问题越少,但性能越低,Spring 支持数据库的4种隔离级别,默认跟随数据库(MySQL 默认为 REPEATABLE READ)。

隔离级别 核心作用(解决的并发问题) 存在的问题 通俗说明
READ UNCOMMITTED(读未提交) 无隔离,允许读取未提交的数据 脏读、不可重复读、幻读(所有问题都存在) 别人没提交的修改,我能读到
READ COMMITTED(读已提交) 禁止读取未提交的数据,解决脏读 不可重复读、幻读 别人提交后的修改,我才能读到,同一事务中多次读结果可能不同
REPEATABLE READ(可重复读,MySQL 默认) 解决脏读、不可重复读,保证同一事务中多次读结果一致 幻读(MySQL 用 MVCC 机制基本解决幻读) 同一事务中,无论别人怎么修改提交,我读到的始终是事务开始时的数据
SERIALIZABLE(串行化) 完全隔离,事务串行执行,解决所有并发问题 性能极低,并发量差(会锁表) 所有事务排队执行,相当于单线程,无并发干扰

“隔离级别越高,并发问题越少,性能越低”,以及 MySQL 默认隔离级别(REPEATABLE READ)。

二、Spring 事务底层原理(AOP + 动态代理) 🔴

核心逻辑:Spring 事务是 基于 AOP 动态代理 实现的,本质是“通过代理对象拦截目标方法,在方法执行前后织入事务的开启、提交、回滚逻辑”,与 AOP 原理一脉相承。

完整流程

  1. 容器启动时,Spring 扫描带有 @Transactional 注解的类/方法,生成代理对象(JDK 动态代理/ CGLIB 代理,与 AOP 代理逻辑一致);

  2. 当调用事务方法时,实际调用的是代理对象的方法,代理对象先拦截目标方法;

  3. 代理对象调用事务管理器(PlatformTransactionManager),开启数据库事务(执行 BEGIN);

  4. 代理对象调用目标方法,执行业务逻辑;

  5. 若目标方法正常执行(无异常),代理对象调用事务管理器提交事务(执行 COMMIT);

  6. 若目标方法抛出异常(符合回滚规则),代理对象调用事务管理器回滚事务(执行 ROLLBACK);

  7. 若异常不符合回滚规则,事务不回滚,直接提交。

关键提醒

  • 事务管理器是核心:Spring 不直接管理事务,而是通过事务管理器适配不同数据库(如 MySQL 用 DataSourceTransactionManager);

  • 与 AOP 关联:事务的“切面”是 @Transactional 注解,“切点”是被注解标记的方法,“通知”是事务的开启、提交、回滚逻辑;

  • 代理对象:与 AOP 代理一致,有接口用 JDK 代理,无接口用 CGLIB 代理,这也是“内部调用事务失效”的核心原因(内部调用未走代理对象)。

三、事务失效的全部场景 🔴

核心原因:事务的 AOP 代理未生效、事务配置错误、数据库不支持事务,共7种场景,重点记前4种(最常见)。

1. 非 Public 方法(最易忽略)

场景:@Transactional 注解标注在 private、protected、default 修饰的方法上,事务失效。

原因:Spring AOP 默认只代理 public 方法,非 public 方法无法被 AOP 拦截,事务逻辑无法织入。

解决方案:将方法改为 public 修饰。

2. 内部调用(最常见)

场景:同一个 Bean 中,非事务方法 A 调用事务方法 B,B 的事务失效。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
@Service
public class UserService {
    // 非事务方法 A
    public void add() {
        // 内部调用,this 指向目标对象,未走代理对象
        this.update(); 
    }
    
    // 事务方法 B(@Transactional 失效)
    @Transactional
    public void update() {
        // 业务逻辑
    }
}

原因:this 指向目标对象,而非代理对象,内部调用未触发 AOP 拦截,事务逻辑未织入。

解决方案:通过 ApplicationContext 获取代理对象,或 @Autowired 注入自身(代理对象),再调用方法。

3. 异常类型错误(最易踩坑)

场景:事务方法抛出 checked 异常(如 IOException、SQLException),事务不回滚;或手动捕获异常,未重新抛出。

原因:Spring 事务默认只对 unchecked 异常(RuntimeException 及其子类) 回滚,checked 异常不回滚;若手动捕获异常,未重新抛出,代理对象无法感知异常,无法触发回滚。

解决方案:① 抛出 unchecked 异常;② 用 @Transactional(rollbackFor = 异常类型.class) 指定回滚异常;③ 捕获异常后,重新抛出异常(throw new RuntimeException(e))。

4. 数据库引擎不支持事务

场景:MySQL 数据库使用 MyISAM 引擎,事务失效。

原因:MyISAM 引擎是非事务引擎,不支持事务;Spring 事务依赖数据库事务,数据库不支持,事务无效。

解决方案:将数据库引擎改为 InnoDB(MySQL 5.5+ 默认是 InnoDB)。

5. 未开启 Spring 事务管理

场景:未加 @EnableTransactionManagement 注解(Spring Boot 自动配置可省略,但手动配置需加),事务失效。

原因:未开启 Spring 事务管理功能,无法扫描 @Transactional 注解,无法生成代理对象。

解决方案:在配置类上加 @EnableTransactionManagement 注解。

6. @Transactional 注解配置错误

  • propagation = NOT_SUPPORTED / NEVER:配置为非事务传播行为,事务不开启;

  • readOnly = true:配置为只读事务,若执行修改操作(insert/update/delete),事务失效;

  • rollbackFor 配置错误:未指定需要回滚的异常类型,导致异常不回滚。

7. 目标对象不是 Spring 容器管理的 Bean

场景:手动 new 出来的对象(如 new UserService()),而非通过 @Autowired 获取的 Bean,事务失效。

原因:手动 new 的对象未被 Spring IOC 管理,无法生成代理对象,事务逻辑无法织入。

解决方案:通过 @Autowired 注入 Bean,或通过 ApplicationContext 获取 Bean。

四、事务配置方式:@Transactional 注解 + 编程式事务对比

Spring 支持两种事务配置方式,实际开发中 注解式事务(@Transactional)最常用,编程式事务用于复杂场景,重点掌握注解式。

1. 注解式事务(推荐,简洁高效)

核心:用 @Transactional 注解标注类/方法,Spring 自动织入事务逻辑,可配置传播行为、隔离级别、回滚规则等。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// 1. 类级别:所有 public 方法都生效
@Service
@Transactional(propagation = Propagation.REQUIRED, isolation = Isolation.REPEATABLE_READ)
public class UserService {
    // 2. 方法级别:覆盖类级别配置(优先级更高)
    @Transactional(rollbackFor = Exception.class) // 所有异常都回滚
    public void addUser() {
        // 业务逻辑(insert/update)
    }
    
    // 只读事务(仅查询,优化性能)
    @Transactional(readOnly = true)
    public User getUserById(Long id) {
        return userDao.selectById(id);
    }
}

关键:方法级别注解优先级高于类级别,常用配置参数:propagation(传播行为)、isolation(隔离级别)、rollbackFor(回滚异常)、readOnly(只读)。

2. 编程式事务(手动控制,复杂场景)

核心:通过 TransactionTemplate 手动控制事务的开启、提交、回滚,适用于需要灵活控制事务的场景(如多事务嵌套、条件提交)。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
@Service
public class UserService {
    @Autowired
    private TransactionTemplate transactionTemplate;
    @Autowired
    private UserDao userDao;
    
    public void addUser() {
        // 手动控制事务
        transactionTemplate.execute(status -> {
            try {
                // 业务逻辑
                userDao.insert(new User());
                return true; // 成功,提交事务
            } catch (Exception e) {
                status.setRollbackOnly(); // 失败,手动回滚
                return false;
            }
        });
    }
}

3. 两种方式对比 🔴

对比维度 注解式事务(@Transactional) 编程式事务(TransactionTemplate)
使用复杂度 简单,只需加注解,无需手动控制 复杂,需手动写代码控制提交/回滚
灵活性 低,无法灵活控制事务流程(如条件回滚) 高,可灵活控制事务的开启、提交、回滚时机
适用场景 绝大多数简单业务场景(CRUD) 复杂场景(多事务嵌套、条件提交/回滚)
耦合度 低,注解式无侵入 高,代码侵入性强

五、事务回滚规则 🔴

核心:明确“哪些异常会回滚、哪些不会”,以及“如何自定义回滚规则”,避免因异常配置错误导致事务不回滚。

1. 默认回滚规则(重点)

  • ✅ 会回滚:所有 unchecked 异常(RuntimeException 及其子类,如 NullPointerException、IllegalArgumentException);

  • ❌ 不会回滚:所有 checked 异常(需要手动捕获的异常,如 IOException、SQLException、ClassNotFoundException);

  • ❌ 不会回滚:手动捕获异常,且未重新抛出异常(代理对象无法感知异常)。

2. 自定义回滚规则(常用)

通过 @Transactional 注解的两个参数,自定义回滚/不回滚的异常:

1
2
3
4
5
6
7
8
// 1. 指定需要回滚的异常(无论 checked/unchecked,只要抛出就回滚)
@Transactional(rollbackFor = {Exception.class, IOException.class})

// 2. 指定不需要回滚的异常(即使抛出,也不回滚)
@Transactional(noRollbackFor = {NullPointerException.class})

// 3. 组合使用(抛出 IOException 回滚,抛出 NullPointerException 不回滚)
@Transactional(rollbackFor = IOException.class, noRollbackFor = NullPointerException.class)

关键提醒:rollbackFor 优先级高于默认规则,建议日常开发中显式配置 rollbackFor = Exception.class,避免因异常类型问题导致事务不回滚。

3. 特殊场景:环绕通知与事务回滚

若用 AOP 环绕通知拦截事务方法,需注意:环绕通知中捕获异常后,必须重新抛出,否则事务无法回滚

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Around("logPointcut()")
public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
    try {
        System.out.println("环绕通知:方法执行前");
        Object result = joinPoint.proceed(); // 调用目标事务方法
        System.out.println("环绕通知:方法执行后");
        return result;
    } catch (Exception e) {
        System.out.println("环绕通知:捕获异常");
        throw e; // 必须重新抛出异常,否则事务不回滚
    }
}

总结 🔴

  1. 事务核心:7种传播行为(重点 REQUIRED/REQUIRES_NEW/NESTED)、4种隔离级别(MySQL 默认 REPEATABLE READ);

  2. 底层原理:AOP + 动态代理,代理对象拦截目标方法,织入事务开启、提交、回滚逻辑;

  3. 事务失效场景(必背):非 public 方法、内部调用、异常类型错误、数据库引擎不支持、未开启事务管理、注解配置错误、非 Spring 管理的 Bean;

  4. 配置方式:注解式(@Transactional,常用)、编程式(TransactionTemplate,复杂场景);

  5. 回滚规则:默认回滚 unchecked 异常,可通过 rollbackFor/noRollbackFor 自定义,环绕通知需重新抛出异常。