Spring事务传播机制深度解析

前言

在Spring开发中,事务管理是保证数据一致性的核心机制。而事务的传播行为(Propagation Behavior)更是面试和实际开发中的高频考点。今天我们就通过一个完整的实战项目,深入理解REQUIRED 、NESTED 和REQUIRES_NEW三种传播类型的区别,并解决在实际使用中遇到的各种坑。

一、什么是事务传播机制?

事务传播机制定义了当一个事务方法被另一个事务方法调用时,事务应该如何传播。Spring提供了7种传播行为:

Spring的7种事务传播类型

传播类型 说明 使用频率
REQUIRED 如果当前存在事务,则加入该事务;否则创建新事务(默认) ⭐⭐⭐⭐⭐
SUPPORTS 如果当前存在事务,则加入该事务;否则以非事务方式执行 ⭐⭐
MANDATORY 如果当前存在事务,则加入该事务;否则抛出异常 ⭐
REQUIRES_NEW 总是创建新事务,如果当前存在事务,则挂起当前事务,运行完再恢复当前事务 ⭐⭐⭐
NOT_SUPPORTED 以非事务方式执行,如果当前存在事务,则挂起当前事务,运行完再恢复当前事务 ⭐
NEVER 以非事务方式执行,如果当前存在事务,则抛出异常 ⭐
NESTED 如果当前存在事务,则在嵌套事务中执行(使用保存点);否则创建新事务 ⭐⭐

🍚 生活化类比:下班回家吃饭

为了让你更好地理解这7种传播类型,我们来用一个下班回家吃饭的场景来类比:

假设你下班回家,老婆在家做饭。不同的传播类型就像不同的"吃饭策略":

0️⃣ REQUIRED - "反正就是要吃饭"

如果老婆做饭了,你就吃老婆做的饭;如果老婆没有做饭,你就自己做饭吃。

💡 核心: 反正你就是要吃饭(反正要在事务中运行)

1️⃣ SUPPORTS - "随缘吃饭"

如果老婆做饭了,你就吃老婆做的饭;如果老婆没有做饭,你就不吃了。

💡 核心: 不一定非要吃饭(不一定非要在事务中运行)

2️⃣ MANDATORY - "家暴男式吃饭"

非要吃老婆做的饭,老婆要是没有做饭,你就大发脾气!

💡 核心: 必须在已有事务中运行,否则抛异常(典型的家暴男😂)

3️⃣ REQUIRES_NEW - "独立做饭"

劳资非要吃自己做的饭,就算老婆把饭做好了,我也不吃老婆做的!

💡 核心: 总要开启新事务,挂起现有事务(我行我素)

4️⃣ NOT_SUPPORTED - "绝食抗议"

劳资就是不吃饭,就算老婆把饭做好了,我也不吃!

💡 核心: 以非事务方式运行,挂起现有事务(坚决不吃饭)

5️⃣ NEVER - "暴躁绝食"

老子就是不吃饭,如果老婆把饭做好了,我还要发脾气!

💡 核心: 必须以非事务方式运行,如果存在事务则抛异常(暴躁老哥😤)

6️⃣ NESTED - "分碗吃饭"

如果老婆做饭了,你就吃老婆做的饭,但是单独盛到自己碗里,避免把自己的口腔微生物传染到桌上的菜碗;如果老婆没有做饭,你就自己做饭吃。

💡 核心: 在嵌套事务中运行(使用保存点),与REQUIRED的区别是把异常影响限制在嵌套事务中(讲究卫生🥗)

其中最常用,且容易混淆,难以理解的是:

  • REQUIRED(默认):适合大多数场景
  • REQUIRES_NEW:需要独立事务的场景
  • NESTED:需要隔离内层异常的场景

本文重点讲解这三种最常用的传播类型。

二、核心问题:方法A调用方法B,异常处理对回滚的影响

这是我们最关心的场景:

复制代码
方法A(@Transactional)
  ├─ 插入数据1
  ├─ 调用方法B(@Transactional)
  │   ├─ 插入数据2
  │   └─ 抛出异常(或不抛出)
  └─ 插入数据3(可能执行,可能不执行)
  └─ 抛出异常(或不抛出)

关键问题:

  1. 如果方法A捕获了方法B的异常,数据会怎样?
  2. 如果方法A不捕获异常,数据会怎样?
  3. REQUIRED、NESTED和REQUIRES_NEW有什么区别?

三、技术栈详解:从JDBC到Spring Data JPA

在深入事务之前,我们先理清Java持久化技术的演进关系。

3.1 持久化技术层次图

复制代码
应用层
  ├── Spring Data JPA (最高层抽象)
  │   └── 基于 JPA 规范
  │       └── 实现:Hibernate、EclipseLink等
  │           └── 底层:JDBC
  │
  ├── MyBatis (独立的ORM框架)
  │   └── 底层:JDBC
  │
  └── Spring JDBC (JDBC的轻量级封装)
      ├── JdbcTemplate
      └── NamedParameterJdbcTemplate
          └── 底层:原生JDBC

基础层
  └── JDBC (Java Database Connectivity)
      └── 数据库驱动 (MySQL Driver、H2 Driver等)
          └── 数据库 (MySQL、H2、Oracle等)

3.2 各技术详解

1️⃣ JDBC (Java Database Connectivity)
  • 性质:Java官方标准API(java.sql包)
  • 作用:提供Java程序访问数据库的统一接口
  • 特点 :
    • ✅ 所有Java数据库操作的基础
    • ✅ 轻量级,性能最好
    • ❌ 需要手动编写SQL
    • ❌ 需要手动处理结果集映射
    • ❌ 代码冗长,容易出错

示例代码:

java 复制代码
// 原生JDBC写法 - 繁琐
Connection conn = dataSource.getConnection();
PreparedStatement ps = conn.prepareStatement(
    "SELECT * FROM account WHERE id = ?"
);
ps.setLong(1, 1L);
ResultSet rs = ps.executeQuery();
Account account = new Account();
if (rs.next()) {
    account.setId(rs.getLong("id"));
    account.setName(rs.getString("name"));
    account.setBalance(rs.getDouble("balance"));
}
rs.close();
ps.close();
conn.close();
2️⃣ Spring JDBC

Spring对JDBC的轻量级封装,简化了资源管理和异常处理。

JdbcTemplate
  • 使用位置参数(?)
  • 自动管理连接、语句和结果集的关闭
  • 统一的异常体系
java 复制代码
// JdbcTemplate写法
String sql = "INSERT INTO account (name, balance) VALUES (?, ?)";
jdbcTemplate.update(sql, "张三", 1000.0);

// 查询
List<Account> accounts = jdbcTemplate.query(
    "SELECT * FROM account", 
    new BeanPropertyRowMapper<>(Account.class)
);
NamedParameterJdbcTemplate ⭐推荐
  • 使用命名参数(:name),更清晰
  • 支持Map和对象作为参数源
  • 特别适合多参数SQL
java 复制代码
// NamedParameterJdbcTemplate写法
String sql = "INSERT INTO account (name, balance) VALUES (:name, :balance)";
Map<String, Object> params = new HashMap<>();
params.put("name", "张三");
params.put("balance", 1000.0);
namedJdbcTemplate.update(sql, params);

// 或使用SqlParameterSource
SqlParameterSource params = new MapSqlParameterSource()
    .addValue("name", "张三")
    .addValue("balance", 1000.0);
namedJdbcTemplate.update(sql, params);

对比:

特性 JdbcTemplate NamedParameterJdbcTemplate
参数类型 位置参数 ? 命名参数 :name
可读性 一般 ✅ 更好
参数复用 ❌ 困难 ✅ 轻松
适用场景 简单SQL 复杂SQL、多参数
3️⃣ JPA (Java Persistence API)
  • 性质:Java官方ORM规范(JSR 338),不是具体实现
  • 作用:定义对象关系映射的标准接口
  • 特点 :
    • ✅ 标准化,可移植性强
    • ✅ 面向对象操作,无需写SQL
    • ✅ 自动管理实体生命周期
    • ❌ 学习曲线较陡
    • ❌ 复杂查询不够灵活

常见实现:

  • Hibernate(最流行,Spring Boot默认)
  • EclipseLink
  • OpenJPA
java 复制代码
// JPA写法
@PersistenceContext
EntityManager em;

Account account = em.find(Account.class, 1L); // 根据ID查询
account.setBalance(2000.0);
em.merge(account); // 更新
4️⃣ Spring Data JPA
  • 性质:Spring对JPA的进一步抽象和简化
  • 作用:减少样板代码,提供Repository模式
  • 特点 :
    • ✅ 几乎不需要写实现代码
    • ✅ 方法名自动生成查询
    • ✅ 支持分页、排序
    • ✅ 与Spring生态完美集成
    • ⚠️ 底层还是JPA(通常是Hibernate)
java 复制代码
// Spring Data JPA写法
public interface AccountRepository extends JpaRepository<Account, Long> {
    // 方法名自动生成SQL,无需实现
    List<Account> findByName(String name);
    List<Account> findByBalanceGreaterThan(Double balance);
    
    // 自定义查询
    @Query("SELECT a FROM Account a WHERE a.balance > :balance")
    List<Account> findRichAccounts(@Param("balance") Double balance);
}

// 使用
@Autowired
AccountRepository repo;

List<Account> accounts = repo.findByName("张三");
5️⃣ MyBatis
  • 性质:独立的持久层框架(Apache基金会)
  • 作用:SQL映射框架,介于JDBC和全自动ORM之间
  • 特点 :
    • ✅ SQL可控性强
    • ✅ 灵活性高,适合复杂查询
    • ✅ 学习成本低
    • ✅ 性能好
    • ❌ 需要手动编写SQL(XML或注解)
    • ❌ 不是JPA标准,不可移植
java 复制代码
// MyBatis Mapper接口
@Mapper
public interface AccountMapper {
    @Select("SELECT * FROM account WHERE id = #{id}")
    Account findById(Long id);
    
    @Insert("INSERT INTO account(name, balance) VALUES(#{name}, #{balance})")
    void insert(Account account);
    
    @Update("UPDATE account SET balance = #{balance} WHERE id = #{id}")
    void update(Account account);
}

// 使用
@Autowired
AccountMapper mapper;

Account account = mapper.findById(1L);

3.3 技术选型对比

特性 JDBC Spring JDBC JPA/Hibernate Spring Data JPA MyBatis
抽象级别 最低 低 高 最高 中
SQL控制 完全控制 完全控制 自动生成 自动生成 手动编写
学习成本 低 低 高 中 低
开发效率 低 中 高 最高 中
性能 最好 很好 较好 较好 好
灵活性 最高 很高 较低 较低 高
可移植性 好 好 好 好 差
适用场景 极致性能 简单项目 企业应用 快速开发 复杂SQL

3.4 选型建议

  • 小型项目/简单CRUD → Spring Data JPA(开发效率最高)
  • 复杂SQL/报表系统 → MyBatis(SQL可控)
  • 高性能要求 → Spring JDBC(轻量级)
  • 微服务/快速迭代 → Spring Data JPA
  • 传统企业系统 → MyBatis(国内生态成熟)
  • 学习/理解底层 → 从JDBC开始

四、Spring事务管理器详解

4.1 事务管理器层次结构

复制代码
PlatformTransactionManager (接口 - Spring事务管理核心)
    ├── DataSourceTransactionManager (JDBC事务管理器)
    ├── JpaTransactionManager (JPA事务管理器)
    ├── HibernateTransactionManager (Hibernate事务管理器)
    └── ... 其他实现

4.2 PlatformTransactionManager

性质:Spring事务管理的核心接口

核心方法:

java 复制代码
public interface PlatformTransactionManager {
    // 获取事务状态
    TransactionStatus getTransaction(TransactionDefinition definition);
    
    // 提交事务
    void commit(TransactionStatus status);
    
    // 回滚事务
    void rollback(TransactionStatus status);
}

作用:

  • 统一的事务管理抽象
  • 屏蔽底层事务实现差异
  • 支持声明式事务(@Transactional)

4.3 DataSourceTransactionManager

适用场景:直接使用JDBC或Spring JDBC的项目

特点:

  • ✅ 基于JDBC Connection的事务管理
  • ✅ 完全支持NESTED传播(通过Savepoint)
  • ✅ 轻量级,性能好
  • ❌ 不支持JPA EntityManager

配置示例:

java 复制代码
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource) {
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource);
        tm.setNestedTransactionAllowed(true); // 启用嵌套事务
        return tm;
    }
}

4.4 JpaTransactionManager

适用场景:使用JPA/Hibernate的项目

特点:

  • ✅ 基于JPA EntityManager的事务管理
  • ✅ 与JPA生命周期集成
  • ⚠️ NESTED支持有限(需要特殊配置)
  • ❌ 配置复杂

配置示例:

java 复制代码
@Configuration
@EnableTransactionManagement
public class TransactionConfig {
    
    @Bean
    public PlatformTransactionManager transactionManager(
            EntityManagerFactory entityManagerFactory) {
        JpaTransactionManager tm = new JpaTransactionManager();
        tm.setEntityManagerFactory(entityManagerFactory);
        
        // 尝试启用保存点支持(不一定成功)
        HibernateJpaDialect jpaDialect = new HibernateJpaDialect();
        jpaDialect.setPrepareConnection(true);
        tm.setJpaDialect(jpaDialect);
        
        return tm;
    }
}

4.5 事务管理器对比

特性 DataSourceTransactionManager JpaTransactionManager
适用技术 JDBC、Spring JDBC JPA、Hibernate
NESTED支持 ✅ 完全支持 ⚠️ 部分支持
配置复杂度 简单 复杂
性能 更好 略差
功能丰富度 基础 丰富
推荐使用 Spring JDBC项目 JPA项目

4.6 为什么选择DataSourceTransactionManager?

在我们的项目中,选择DataSourceTransactionManager的原因:

  1. 完全支持NESTED传播:通过JDBC Savepoint机制
  2. 简单可靠:配置少,不易出错
  3. 性能好:没有JPA的额外开销
  4. 适合演示:专注于事务传播,不涉及ORM复杂性

五、REQUIRED传播类型实战

5.1 测试场景1:捕获异常

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testRequiredWithCatch() {
    // 插入第一条数据
    accountDao.insert(new Account("REQUIRED_CATCH_A", 1000.0));
    
    try {
        methodB_Required(); // REQUIRED传播
    } catch (RuntimeException e) {
        System.out.println("捕获到异常: " + e.getMessage());
    }
    
    // ❌ 这之前会报错:Transaction silently rolled back
    accountDao.insert(new Account("REQUIRED_CATCH_B", 2000.0));
}

@Transactional(propagation = Propagation.REQUIRED)
public void methodB_Required() {
    accountDao.insert(new Account("REQUIRED_METHOD_B", 500.0));
    throw new RuntimeException("方法B异常");
}

测试结果:

json 复制代码
{
  "success": false,
  "error": "Transaction silently rolled back because it has been marked as rollback-only"
}

结论: ❌ 即使捕获了异常,整个事务也会回滚

原因分析:

  1. REQUIRED传播下,方法A和方法B共享同一个事务
  2. 方法B抛出异常时,事务被标记为rollback-only
  3. 即使方法A捕获了异常,事务状态已无法改变
  4. 任何后续的数据库操作都会失败

5.2 测试场景2:不捕获异常

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testRequiredWithoutCatch() {
    accountDao.insert(new Account("REQUIRED_NO_CATCH_A", 1000.0));
    
    methodB_Required(); // 抛出异常,不捕获
    
    // ❌ 这行不会执行
    accountDao.insert(new Account("REQUIRED_NO_CATCH_B", 2000.0));
}

测试结果:

json 复制代码
{
  "success": false,
  "error": "方法B发生异常 - REQUIRED传播",
  "data": []  // 所有数据都回滚
}

结论: ❌ 所有数据都回滚

5.3 REQUIRED传播总结

场景 结果 原因
内层异常 + 外层catch ❌ 全部回滚 事务标记为rollback-only
内层异常 + 外层不catch ❌ 全部回滚 异常传播导致回滚
内层正常 + 外层异常 ❌ 全部回滚 同一事务,同生共死

核心特点:

  • ✅ 简单直接,适合大多数场景
  • ✅ 保证事务的原子性
  • ❌ 无法隔离内层异常
  • ❌ 一旦标记rollback-only,无法恢复

记忆口诀: REQUIRED = 同生共死


六、NESTED传播类型实战

6.1 什么是保存点(Savepoint)?

NESTED传播的核心是数据库保存点机制:

复制代码
事务开始
  ├─ 插入数据1
  ├─ 【创建保存点SP1】
  ├─ 插入数据2(嵌套事务)
  ├─ 抛出异常
  └─ 【回滚到保存点SP1】← 只回滚数据2,数据1保留

6.2 测试场景1:捕获异常

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testNestedWithCatch() {
    // 插入第一条数据
    accountDao.insert(new Account("NESTED_CATCH_A", 1000.0));
    
    try {
        methodB_Nested(); // NESTED传播
    } catch (RuntimeException e) {
        System.out.println("捕获到异常: " + e.getMessage());
    }
    
    // ✅ 可以正常执行
    accountDao.insert(new Account("NESTED_CATCH_B", 2000.0));
}

@Transactional(propagation = Propagation.NESTED)
public void methodB_Nested() {
    accountDao.insert(new Account("NESTED_METHOD_B", 500.0));
    throw new RuntimeException("方法B异常");
}

测试结果:

json 复制代码
{
  "success": true,
  "message": "NESTED with catch - 只有account1和account2应该保存,methodB的数据应该回滚",
  "data": [
    {"id": 1, "name": "NESTED_CATCH_A", "balance": 1000.0},
    {"id": 3, "name": "NESTED_CATCH_B", "balance": 2000.0}
  ]
}

结论: ✅ 方法A的数据保存,方法B的数据回滚

原因分析:

  1. 方法A开启事务
  2. 方法B执行前创建保存点
  3. 方法B抛出异常,回滚到保存点(只回滚方法B的操作)
  4. 方法A捕获异常,继续执行
  5. 方法A提交事务,保存点之前的数据生效

6.3 测试场景2:不捕获异常

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testNestedWithoutCatch() {
    accountDao.insert(new Account("NESTED_NO_CATCH_A", 1000.0));
    
    methodB_Nested(); // 抛出异常,不捕获
    
    // ❌ 这行不会执行
    accountDao.insert(new Account("NESTED_NO_CATCH_B", 2000.0));
}

测试结果:

json 复制代码
{
  "success": false,
  "error": "方法B发生异常 - NESTED传播",
  "data": []  // 所有数据都回滚
}

结论: ❌ 所有数据都回滚

原因分析:

虽然NESTED可以回滚到保存点,但异常会继续传播到方法A,导致外层事务也回滚。

6.4 测试场景3:内层正常,外层异常

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testNestedOuterException() {
    accountDao.insert(new Account("NESTED_OUTER_A", 1000.0));
    
    methodB_Nested_Normal(); // NESTED传播,正常执行
    
    accountDao.insert(new Account("NESTED_OUTER_B", 2000.0));
    
    // 外层抛出异常
    throw new RuntimeException("方法A异常");
}

@Transactional(propagation = Propagation.NESTED)
public void methodB_Nested_Normal() {
    accountDao.insert(new Account("NESTED_B_NORMAL", 500.0));
    // 正常执行,不抛出异常
}

测试结果:

json 复制代码
{
  "success": false,
  "error": "方法A发生异常 - 外层异常导致全部回滚",
  "data": []  // 所有数据都回滚
}

结论: ❌ 所有数据都回滚(包括内层正常执行的数据)

原因分析:

  1. 方法B的数据虽然在保存点之后提交
  2. 但外层事务最终回滚,所有操作都失效
  3. 保存点只能向内回滚,不能向外隔离

6.5 NESTED传播总结

场景 结果 原因
内层异常 + 外层catch ✅ 内层回滚,外层保存 保存点机制生效
内层异常 + 外层不catch ❌ 全部回滚 异常传播到外层
内层正常 + 外层异常 ❌ 全部回滚 外层回滚影响所有

核心特点:

  • ✅ 可以隔离内层异常(通过保存点)
  • ✅ 外层可以继续执行
  • ❌ 不能隔离外层异常
  • ⚠️ 需要数据库支持保存点(H2、MySQL、PostgreSQL都支持)

记忆口诀: NESTED = 内外有别,但不能逆天

类比理解:

保存点就像游戏中的存档点

  • 你可以读档回到之前的状态(内层回滚)
  • 但如果游戏本身崩溃了(外层异常),所有进度都会丢失

七、REQUIRES_NEW传播类型实战

7.1 什么是独立事务?

REQUIRES_NEW传播的核心是完全独立的事务:

复制代码
外层事务(挂起)
  └─ 内层事务(独立)
      ├─ 插入数据
      ├─ 提交/回滚(不影响外层)
  └─ 外层事务(恢复)
      └─ 继续执行

7.2 测试场景1:内层正常,外层异常

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testRequiresNewOuterException() {
    // 外层事务
    accountDao.insert(new Account("REQ_NEW_OUTER_A", 1000.0));
    
    // 内层独立事务(外层事务被挂起)
    methodB_RequiresNew_Normal();
    
    // 外层事务(恢复)
    accountDao.insert(new Account("REQ_NEW_OUTER_B", 2000.0));
    
    // 外层抛出异常
    throw new RuntimeException("方法A异常");
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB_RequiresNew_Normal() {
    accountDao.insert(new Account("REQ_NEW_B_NORMAL", 500.0));
    // 正常执行,独立事务提交
}

测试结果:

json 复制代码
{
  "success": false,
  "error": "方法A发生异常 - 外层异常导致外层回滚,但内层已提交",
  "data": [
    {"id": 1, "name": "REQ_NEW_B_NORMAL", "balance": 500.0}
  ]
}

结论: ✅ 方法B的数据保存,方法A的数据回滚

原因分析:

  1. 方法A开启事务T1
  2. 方法B执行时,挂起T1,创建新事务T2
  3. 方法B执行完成,T2独立提交
  4. 恢复T1,继续执行
  5. 方法A抛出异常,T1回滚
  6. T2已提交,不受T1回滚影响

7.3 测试场景2:内层异常,外层捕获

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testRequiresNewInnerExceptionWithCatch() {
    accountDao.insert(new Account("REQ_NEW_CATCH_A", 1000.0));
    
    try {
        methodB_RequiresNew_Exception(); // 独立事务,抛出异常
    } catch (RuntimeException e) {
        System.out.println("捕获到异常: " + e.getMessage());
    }
    
    // ✅ 可以正常执行
    accountDao.insert(new Account("REQ_NEW_CATCH_B", 2000.0));
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB_RequiresNew_Exception() {
    accountDao.insert(new Account("REQ_NEW_B_EXCEPTION", 500.0));
    throw new RuntimeException("方法B异常");
}

测试结果:

json 复制代码
{
  "success": true,
  "message": "REQUIRES_NEW with catch - 外层数据保存,内层数据回滚",
  "data": [
    {"id": 1, "name": "REQ_NEW_CATCH_A", "balance": 1000.0},
    {"id": 2, "name": "REQ_NEW_CATCH_B", "balance": 2000.0}
  ]
}

结论: ✅ 方法A的数据保存,方法B的数据回滚

原因分析:

  1. 方法B的独立事务回滚(不影响外层)
  2. 方法A捕获异常,继续执行
  3. 方法A的外层事务正常提交

7.4 测试场景3:内层异常,外层不捕获

java 复制代码
@Transactional(propagation = Propagation.REQUIRED)
public String testRequiresNewInnerExceptionWithoutCatch() {
    accountDao.insert(new Account("REQ_NEW_NO_CATCH_A", 1000.0));
    
    methodB_RequiresNew_Exception(); // 抛出异常,不捕获
    
    // ❌ 这行不会执行
    accountDao.insert(new Account("REQ_NEW_NO_CATCH_B", 2000.0));
}

测试结果:

json 复制代码
{
  "success": false,
  "error": "方法B发生异常 - REQUIRES_NEW传播",
  "data": []  // 所有数据都回滚
}

结论: ❌ 所有数据都回滚

原因分析:

  1. 方法B的独立事务回滚
  2. 异常传播到方法A
  3. 方法A的外层事务也回滚

7.5 REQUIRES_NEW传播总结

场景 结果 原因
内层异常 + 外层catch ✅ 内层回滚,外层保存 事务完全独立
内层异常 + 外层不catch ❌ 全部回滚 异常传播到外层
内层正常 + 外层异常 ✅ 内层保存,外层回滚 事务完全独立

核心特点:

  • ✅ 创建完全独立的事务
  • ✅ 内层提交后不受外层影响
  • ✅ 可以隔离内层异常
  • ❌ 不能阻止异常传播(除非捕获)
  • ⚠️ 性能开销较大(需要挂起/恢复事务)

记忆口诀: REQUIRES_NEW = 各自为政,互不干涉

类比理解:

REQUIRES_NEW就像开两个独立的银行账户

  • 账户A存钱(外层事务)
  • 账户B存钱(内层独立事务)
  • 账户B的钱存进去就确定了,不受账户A影响

八、三种传播类型终极对比

8.1 综合对比表

场景 REQUIRED NESTED REQUIRES_NEW
内层异常+外层catch ❌ 全部回滚 (rollback-only) ✅ 内层回滚 外层保存 ✅ 内层回滚 外层保存
内层异常+外层不catch ❌ 全部回滚 ❌ 全部回滚 ❌ 全部回滚
内层正常+外层异常 ❌ 全部回滚 ❌ 全部回滚 ✅ 内层保存 外层回滚
事务关系 同一事务 保存点 独立事务
性能开销 最小 较小 较大
使用频率 ⭐⭐⭐⭐⭐ ⭐⭐ ⭐⭐⭐

8.2 选择指南

使用REQUIRED的场景:

  • ✅ 大多数业务场景
  • ✅ 需要保证原子性
  • ✅ 简单的CRUD操作

使用NESTED的场景:

  • ✅ 需要隔离内层异常
  • ✅ 希望外层能继续执行
  • ✅ 数据库支持保存点

使用REQUIRES_NEW的场景:

  • ✅ 日志记录(无论主事务是否成功)
  • ✅ 审计追踪
  • ✅ 发送通知/邮件
  • ✅ 需要立即提交的场景

8.3 决策流程图

复制代码
方法A调用方法B,方法B需要事务吗?
  ├─ 否 → 不使用@Transactional
  └─ 是 ↓
    
方法B的异常应该影响方法A吗?
  ├─ 是 → 使用REQUIRED(默认)
  └─ 否 ↓
    
方法B需要立即提交吗?
  ├─ 是 → 使用REQUIRES_NEW
  └─ 否 ↓
    
只需要隔离内层异常?
  ├─ 是 → 使用NESTED
  └─ 否 → 使用REQUIRED

九、完整测试命令

9.1 启动应用

bash 复制代码
mvn spring-boot:run

9.2 测试REQUIRED传播

bash 复制代码
# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试1:REQUIRED + catch异常(全部回滚)
echo "=== 测试1: REQUIRED + catch ==="
curl http://localhost:8080/api/transaction/required/with-catch

# 查看数据(应该为空)
curl http://localhost:8080/api/transaction/accounts

9.3 测试NESTED传播

bash 复制代码
# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试2:NESTED + catch异常(只有外层保存)
echo "=== 测试2: NESTED + catch ==="
curl http://localhost:8080/api/transaction/nested/with-catch

# 查看数据(应该有NESTED_CATCH_A和NESTED_CATCH_B)
curl http://localhost:8080/api/transaction/accounts

# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试3:NESTED + 不catch异常(全部回滚)
echo "=== 测试3: NESTED + 不catch ==="
curl http://localhost:8080/api/transaction/nested/without-catch

# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试4:NESTED + 外层异常(全部回滚)
echo "=== 测试4: NESTED + 外层异常 ==="
curl http://localhost:8080/api/transaction/nested/outer-exception

9.4 测试REQUIRES_NEW传播

bash 复制代码
# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试5:REQUIRES_NEW + 内层正常,外层异常(只有内层保存)
echo "=== 测试5: REQUIRES_NEW + 外层异常 ==="
curl http://localhost:8080/api/transaction/requires-new/outer-exception

# 查看数据(应该只有REQ_NEW_B_NORMAL)
curl http://localhost:8080/api/transaction/accounts

# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试6:REQUIRES_NEW + 内层异常,外层捕获(只有外层保存)
echo "=== 测试6: REQUIRES_NEW + 内层异常 + catch ==="
curl http://localhost:8080/api/transaction/requires-new/inner-exception-catch

# 查看数据(应该有REQ_NEW_CATCH_A和REQ_NEW_CATCH_B)
curl http://localhost:8080/api/transaction/accounts

# 清空数据
curl -X DELETE http://localhost:8080/api/transaction/clear

# 测试7:REQUIRES_NEW + 内层异常,外层不捕获(全部回滚)
echo "=== 测试7: REQUIRES_NEW + 内层异常 + 不catch ==="
curl http://localhost:8080/api/transaction/requires-new/inner-exception-no-catch

# 查看数据(应该为空)
curl http://localhost:8080/api/transaction/accounts

9.5 H2控制台

访问 http://localhost:8080/h2-console

  • JDBC URL: jdbc:h2:mem:testdb
  • 用户名: sa
  • 密码: (空)

十、常见问题与陷阱

10.1 自注入问题

问题: Service内部方法调用,事务不生效

错误示例:

java 复制代码
@Service
public class MyService {
    @Transactional
    public void methodA() {
        methodB(); // ❌ 直接调用,事务不生效
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // ...
    }
}

解决方案: 使用@Lazy自注入

java 复制代码
@Service
public class MyService {
    @Autowired
    @Lazy
    private MyService self;
    
    @Transactional
    public void methodA() {
        self.methodB(); // ✅ 通过代理调用,事务生效
    }
    
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void methodB() {
        // ...
    }
}

10.2 JPA不支持NESTED

问题: 使用JpaTransactionManager时,NESTED传播报错

解决方案:

  1. 使用DataSourceTransactionManager
  2. 或配置HibernateJpaDialect(不一定成功)
  3. 或改用REQUIRES_NEW

10.3 H2数据库获取自增ID

问题: SCOPE_IDENTITY()函数不存在

解决方案: 使用GeneratedKeyHolder

java 复制代码
GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
namedJdbcTemplate.update(sql, params, keyHolder);
Long id = keyHolder.getKey().longValue();

10.4 rollback-only陷阱

问题: REQUIRED传播中,捕获异常后仍然报错

原因: 事务已被标记为rollback-only

解决方案:

  1. 使用REQUIRES_NEW代替NESTED
  2. 或在捕获异常后手动清除rollback-only标记(不推荐)

十一、最佳实践总结

11.1 技术选型建议

持久化技术:

  • 快速开发 → Spring Data JPA
  • 复杂SQL → MyBatis
  • 简单项目 → Spring JDBC
  • 学习理解 → 从JDBC开始

事务管理器:

  • Spring JDBC → DataSourceTransactionManager
  • JPA项目 → JpaTransactionManager
  • 需要NESTED → DataSourceTransactionManager

11.2 传播类型选择

  • 90%场景 → REQUIRED(默认)
  • 需要隔离内层异常 → NESTED或REQUIRES_NEW
  • 需要独立提交 → REQUIRES_NEW
  • 日志/审计 → REQUIRES_NEW

11.3 编码规范

  1. ✅ 优先使用声明式事务(@Transactional)
  2. ✅ Service层控制事务,DAO层不控制
  3. ✅ 避免在循环中频繁开启事务
  4. ✅ 合理设置事务超时时间
  5. ✅ 注意自注入问题(使用@Lazy)
  6. ✅ 单元测试要覆盖事务场景

11.4 性能优化

  1. 缩小事务范围(只包裹必要的代码)
  2. 避免在事务中进行远程调用
  3. 避免在事务中进行耗时操作
  4. 合理使用REQUIRES_NEW(开销较大)
  5. 批量操作放在一个事务中

十二、结语

通过这个项目,我们不仅深入理解了Spring事务传播机制的本质,还掌握了:

✅ 持久化技术栈 :从JDBC到Spring Data JPA的完整演进

✅ 事务管理器 :PlatformTransactionManager及其实现

✅ 三种传播类型 :REQUIRED、NESTED、REQUIRES_NEW的实战应用

✅ 常见问题 :自注入、rollback-only、保存点支持等

✅ 最佳实践:技术选型、编码规范、性能优化

核心要点回顾:

  • REQUIRED = 同生共死(同一事务)
  • NESTED = 内外有别,但不能逆天(保存点)
  • REQUIRES_NEW = 各自为政,互不干涉(独立事务)

希望这篇文章能帮助你更好地理解Spring事务传播机制,在实际开发中做出正确的技术选型!


源码地址: https://github.com/mojinchuan/transaction-demo

欢迎点赞、收藏、转发! 🎉

参考资料:

  • Spring Framework官方文档
  • JPA规范(JSR 338)
  • Hibernate用户指南
  • 《Spring实战》第5版
相关推荐
细嗅蔷薇@6 小时前
DML常见操作
数据库·mysql
萧瑟余晖7 小时前
Spring IoC容器与Bean生命周期详解
spring
lusklusklusk7 小时前
Oracle数据库基础之7_备份恢复基础之SCN
数据库·oracle
Wx-bishekaifayuan7 小时前
springboot房屋租赁系统11574-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·sql·spring·课程设计
三8447 小时前
Fastjson 漏洞学习笔记 · 03 · 经典利用链:TemplatesImpl 与 JdbcRowSetImpl
java·web安全·fastjson
ao-weilai7 小时前
MySQL数据库:MySQL基础概要
数据库·mysql
雪碧聊技术7 小时前
从 0 跑通一个最小 RAG Demo:文档切分、向量检索与答案生成
数据库·oracle
喜欢的名字被抢了7 小时前
08-Redis 性能优化篇:快在哪、BigKey、HotKey、慢查询与内存
数据库·redis·性能优化
阳光九叶草LXGZXJ8 小时前
达梦数据库-学习-68-dmasm0X_XXXXXX.log日志激增
linux·运维·数据库·sql·学习
蜗牛互联网8 小时前
GPT-6.1 Sol迁移指南:从token单价转向每任务成本门禁
java·人工智能·后端·gpt