Spring事务传播行为

4,498 阅读5分钟

Spring事务传播行为

Spring 的事务传播机制(Transaction Propagation)定义了多个事务方法相互调用时 事务如何传播与协作的规则。Spring 提供了 7 种传播行为,通过 Propagation[ˌprɒpə'ɡeɪʃ(ə)n] 枚举定义,常用的是前两种。

7中行为

传播行为含义说明
required如果当前存在事务,则加入;否则新建一个事务。最常用,适合大多数业务场景。
requires_new总是新建一个事务,如果当前存在事务,则挂起当前事务。新事务独立提交/回滚,适合隔离性强的方法。
supports如果当前存在事务,则加入;否则以非事务方式执行。不强制事务,适合查询类操作。
not_supported以非事务方式执行,如果当前存在事务,则挂起当前事务。明确不使用事务。
mandatory必须在已有事务中运行,否则抛出异常。强制依赖外层事务。
never必须在非事务环境中运行,否则抛出异常。禁止事务。
nested如果当前存在事务,则在嵌套事务中执行;否则新建事务。嵌套事务可独立回滚,但依赖数据库支持(如 JDBC savepoint)。

REQUIRED 实例

下面给出 Spring REQUIRED 的“最小可运行”例子,一条 SQL 就能看见效果
复制到任何 Spring Boot 工程(含 jdbc + h2)即可直接跑通。


  1. 依赖(已有可跳过)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

  1. 建表脚本(resources/schema.sql)
CREATE TABLE IF NOT EXISTS t_log(
   id   INT PRIMARY KEY,
   msg  VARCHAR(100)
);

  1. 业务代码(一个类搞定)
package demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class RequiredDemoService {

    @Autowired
    private JdbcTemplate jdbc;

    // 外层事务( REQUIRED )
    @Transactional
    public void outer() {
        jdbc.update("INSERT INTO t_log(id,msg) VALUES (1,'outer')");
        inner();          // 调用同事务的方法
    }

    // 内层事务( REQUIRED )——加入当前事务
    @Transactional
    public void inner() {
        jdbc.update("INSERT INTO t_log(id,msg) VALUES (2,'inner')");
        // 模拟运行时异常
        throw new RuntimeException("inner error");
    }
}

  1. 单元测试(看回滚效果)
package demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
class RequiredTest {

    @Autowired
    private RequiredDemoService service;

    @Autowired
    private JdbcTemplate jdbc;

    @Test
    void requiredRollback() {
        int before = jdbc.queryForObject("SELECT COUNT(*) FROM t_log", Integer.class);
        try {
            service.outer();          // 两个 INSERT + 异常
        } catch (RuntimeException ignored) {}
        int after = jdbc.queryForObject("SELECT COUNT(*) FROM t_log", Integer.class);
        // 因为共用同一个事务,异常后两条都回滚
        assertEquals(before, after);  // 0 == 0
    }
}

  1. 运行测试

  2. 直接跑 RequiredTest

  3. 日志关键行:

    Creating new transaction
    Executing SQL: INSERT INTO t_log(id,msg) VALUES (1,'outer')
    Executing SQL: INSERT INTO t_log(id,msg) VALUES (2,'inner')
    Rolling back
    
  4. 结论:
    内外方法共用同一个事务inner() 抛异常 → 两条 INSERT 全部回滚,数据保持 0 条。


REQUIRES_NEW 实例

下面给出 Spring REQUIRES_NEW 的“最小可运行”例子。
目标:外层方法有事务,内层方法用 REQUIRES_NEW 自己新开一个事务;
内层回滚不影响外层,外层最终仍能提交。


  1. 依赖(同前面示例,可跳过)
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

  1. 建表脚本(resources/schema.sql)
CREATE TABLE IF NOT EXISTS t_pay(
   id   INT PRIMARY KEY,
   amount INT
);

  1. 业务代码(一个类搞定)
package demo;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

@Service
public class RequiresNewDemoService {

    @Autowired
    private JdbcTemplate jdbc;

    // 外层事务
    @Transactional
    public void outer() {
        jdbc.update("INSERT INTO t_pay(id,amount) VALUES (1,100)");
        try {
            inner();          // 挂起当前事务,新建独立事务
        } catch (RuntimeException e) {
            // 捕获,让外层继续
        }
        // 外层继续提交
    }

    // 内层:REQUIRES_NEW —— 完全独立事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void inner() {
        jdbc.update("INSERT INTO t_pay(id,amount) VALUES (2,200)");
        throw new RuntimeException("inner fail");
    }
}

  1. 单元测试(看“部分提交”效果)
package demo;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.JdbcTemplate;

import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest
class RequiresNewTest {

    @Autowired
    private RequiresNewDemoService service;

    @Autowired
    private JdbcTemplate jdbc;

    @Test
    void requiresNewCommit() {
        service.outer();
        int rows = jdbc.queryForObject("SELECT COUNT(*) FROM t_pay", Integer.class);
        // 外层提交 1 条,内层回滚 0 条
        assertEquals(1, rows);
    }
}

  1. 运行测试

  2. 直接跑 RequiresNewTest

  3. 日志关键行:

    Creating new transaction for outer
    Suspending current transaction
    Creating new transaction for inner
    Rolling back inner transaction
    Resuming suspended transaction
    Committing outer transaction
    
  4. 结论:
    内层独立事务回滚,不影响外层事务提交;数据库最终只剩 id=1 这一条记录。


nested 和 requires_new区别

nested:在大部分的数据库中,一段SQL语句中可以设置一个标志位,运行后面的SQL语句时如果有问题,只回滚到这个标志位的数据状态,而不会让这个标志位之前的SQL语句也回滚。这个标志位在数据库概念中被称为保存点save point)。

虽然 NESTED 和 REQUIRES_NEW 都能“开出一条新执行路径”,但事务边界、回滚范围、提交时机、对外可见性完全不同,使用效果并不等价,选型错误会直接破坏数据一致性。

  1. 事务从属关系
  • NESTED:子事务,仍是外层事务的组成部分,必须等外层最终 commit 才算真正落地
  • REQUIRES_NEW:完全独立的兄弟事务,自己先 commit/rollback,与外层无先后依赖。
  1. 回滚粒度
  • NESTED 异常时只 rollback to savepoint外层可继续运行并最终提交 → 实现“局部失败”。
  • REQUIRES_NEW 异常时 整事务回滚,外层如果 catch 住异常,不影响外层;但外层 rollback 时不会牵连它(因为它已先提交)。
  1. 数据可见性
  • NESTED 在内层“提交”后,其他并发事务仍看不到,因为外层还没 commit。
  • REQUIRES_NEW 一旦内层 commit,立即对外可见(隔离级别允许的前提下)。
  1. 资源开销
  • NESTED 仅创建一个 JDBC Savepoint,轻量。
  • REQUIRES_NEW 要 挂起/获取连接、新建事务上下文,成本更高。
  1. 典型误用案例
  • 想用 REQUIRES_NEW 做“可回滚的试算”,结果内层一提交,外层再回滚也追不回来,造成脏数据
  • 把 NESTED 当“独立日志”用,外层回滚后子事务也被全盘回滚,日志照样丢失。