> 文章列表 > 【学习笔记】Spring事务的传播行为详解

【学习笔记】Spring事务的传播行为详解

【学习笔记】Spring事务的传播行为详解

什么是事务的传播行为 Propagetion

模拟一种场景:方法A和B都带有事务注解,其中A调用B,会发生什么? 事务将会如何传递?是合并成一个事务,还是开启另一个新事务呢?这就是事务的传播行为。

一、Spring定义了一个枚举,一共有七种传播行为:

  • REQUIRED:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】

    默认的传播行为:只要主方法有事务,调用的方法一定会开启事务,并加入到主方法的事务中

  • SUPPORTS:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】

  • MANDATORY:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】

  • REQUIRES_NEW:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】

    简单理解,只要主方法有事务,调用的方法一定会开启一个新事务,而且是不相干的事务

  • NOT_SUPPORTED:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】

  • NEVER:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】

  • NESTED:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

二、枚举类型如下:

【学习笔记】Spring事务的传播行为详解

用实际的场景来模拟事务的传播行为

一、场景描述

  • 场景是一个常见的银行转账场景,数据库的表结构就是用户名+余额即可。
  • 为了测试事务的传播性:我们声明了两个不同的业务A和B
  • A调用了B,且A已经配置了Spring的事务
  • 为了便于测试,省略了表现层

二、搭建测试环境

  1. 创建Spring项目 引入一些必要的依赖

    <?xml version="1.0" encoding="UTF-8"?>
    <project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><groupId>com.powernode</groupId><artifactId>spring6-013-tx-bank</artifactId><version>1.0-SNAPSHOT</version><packaging>jar</packaging><!--依赖--><dependencies><!--spring context--><dependency><groupId>org.springframework</groupId><artifactId>spring-context</artifactId><version>5.2.12.RELEASE</version></dependency><!--spring jdbc--><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.12.RELEASE</version></dependency><!--mysql驱动--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>8.0.20</version></dependency><dependency><groupId>org.springframework</groupId><artifactId>spring-jdbc</artifactId><version>5.2.7.RELEASE</version></dependency><!--德鲁伊连接池--><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.2.13</version></dependency><!--@Resource注解--><dependency><groupId>jakarta.annotation</groupId><artifactId>jakarta.annotation-api</artifactId><version>2.1.1</version></dependency><!--junit--><dependency><groupId>junit</groupId><artifactId>junit</artifactId><version>4.13.2</version><scope>test</scope></dependency><!--log4j2的依赖--><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-core</artifactId><version>2.19.0</version></dependency><dependency><groupId>org.apache.logging.log4j</groupId><artifactId>log4j-slf4j2-impl</artifactId><version>2.19.0</version></dependency></dependencies><properties><maven.compiler.source>8</maven.compiler.source><maven.compiler.target>8</maven.compiler.target></properties></project>
    

    配置文件:

    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:context="http://www.springframework.org/schema/context"xmlns:tx="http://www.springframework.org/schema/tx"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsdhttp://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd"><!--组件扫描--><context:component-scan base-package="com.powernode.bank"/><!--配置数据源--><!--<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">--><!--    <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/>--><!--    <property name="url" value="jdbc:mysql://localhost:3306/bank_account"/>--><!--    <property name="username" value="root"/>--><!--    <property name="password" value="root"/>--><!--</bean>--><!-- 1.配置数据源 --><bean id="dataSource"class="org.springframework.jdbc.datasource.DriverManagerDataSource"><!-- 1.1.数据库驱动 --><property name="driverClassName"value="com.mysql.cj.jdbc.Driver"></property><!-- 1.2.连接数据库的url --><property name="url"value="jdbc:mysql://localhost:3306/bank-tx?characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=UTC&amp;rewriteBatchedStatements=true"></property><!-- 1.3.连接数据库的用户名 --><property name="username" value="root"></property><!-- 1.4.连接数据库的密码 --><property name="password" value="root"></property></bean><!--配置JdbcTemplate--><bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate"><property name="dataSource" ref="dataSource"/></bean><!--配置事务管理器--><bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"><property name="dataSource" ref="dataSource"/></bean><!--开启事务注解驱动器,开启事务注解。告诉Spring框架,采用注解的方式去控制事务。--><tx:annotation-driven transaction-manager="txManager"/></beans>
    

    log4j的配置文件

    <?xml version="1.0" encoding="UTF-8"?><configuration><loggers><!--level指定日志级别,从低到高的优先级:ALL < TRACE < DEBUG < INFO < WARN < ERROR < FATAL < OFF--><root level="DEBUG"><appender-ref ref="spring6log"/></root></loggers><appenders><!--输出日志信息到控制台--><console name="spring6log" target="SYSTEM_OUT"><!--控制日志输出的格式--><PatternLayout pattern="%d{yyyy-MM-dd HH:mm:ss SSS} [%t] %-3level %logger{1024} - %msg%n"/></console></appenders></configuration>
    
  2. 创建数据库。sql如下

    SET NAMES utf8mb4;
    SET FOREIGN_KEY_CHECKS = 0;-- ----------------------------
    -- Table structure for bank-account
    -- ----------------------------
    DROP TABLE IF EXISTS `bank_account`;
    CREATE TABLE `bank_accountt`  (`account` varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL,`balance` decimal(50, 4) NULL DEFAULT NULL,PRIMARY KEY (`account`) USING BTREE
    ) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_bin ROW_FORMAT = Dynamic;-- ----------------------------
    -- Records of bank-account
    -- ----------------------------
    INSERT INTO `bank-account` VALUES ('act-01', 10000.0000);
    INSERT INTO `bank-account` VALUES ('act-02', 5000.0000);SET FOREIGN_KEY_CHECKS = 1;
  3. 写简单的dao层和实体类

    package com.powernode.bank.pojo;import java.math.BigDecimal;public class BankAccount {private String account;private BigDecimal balance;public BankAccount(String account, BigDecimal balance) {this.account = account;this.balance = balance;}public BankAccount() {}public String getAccount() {return account;}public void setAccount(String account) {this.account = account;}@Overridepublic String toString() {return "BankAccount{" +"account='" + account + '\\'' +", balance=" + balance +'}';}public BigDecimal getBalance() {return balance;}public void setBalance(BigDecimal balance) {this.balance = balance;}
    }

    dao层

    package com.powernode.bank.dao.impl;import com.powernode.bank.dao.BankAccountDao;
    import com.powernode.bank.pojo.BankAccount;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.jdbc.core.JdbcTemplate;
    import org.springframework.stereotype.Component;@Component("bankAccountDao")
    public class BankAccountImpl implements BankAccountDao {@Autowired@Qualifier("jdbcTemplate")private JdbcTemplate jdbcTemplate;@Overridepublic int insert(BankAccount bankAccount) {String sql = "insert into bank_account values(?,?)";return jdbcTemplate.update(sql, bankAccount.getAccount() , bankAccount.getBalance());}
    }

    实体类

  4. 写两个简单的业务类如下

    package com.powernode.bank.service.impl;import com.powernode.bank.dao.BankAccountDao;
    import com.powernode.bank.pojo.BankAccount;
    import com.powernode.bank.service.BankAccountService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;@Service("BankAccountService")
    public class BankAccountServiceImpl implements BankAccountService {@Autowiredprivate BankAccountDao bankAccountDao;@Autowired@Qualifier("VIPAccountServiceImpl")private BankAccountService bankAccountService;@Override@Transactionalpublic void insert(BankAccount bankAccount) {System.out.println("===========INSERT BANK ACCOUNT:"+bankAccount);bankAccountDao.insert(bankAccount);bankAccountService.insert(bankAccount);}
    }
    package com.powernode.bank.service.impl;import com.powernode.bank.dao.BankAccountDao;
    import com.powernode.bank.pojo.BankAccount;
    import com.powernode.bank.service.BankAccountService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.beans.factory.annotation.Qualifier;
    import org.springframework.stereotype.Service;import java.math.BigDecimal;@Service("VIPAccountServiceImpl")
    public class VIPAccountServiceImpl implements BankAccountService {@Autowired@Qualifier("bankAccountDao")private BankAccountDao bankAccountDao;@Overridepublic void insert(BankAccount bankAccount) {bankAccount.setAccount("VIP" + bankAccount.getAccount());bankAccount.setBalance(bankAccount.getBalance().add(new BigDecimal("99999.00")));System.out.println("===========INSERT VIP ACCOUNT:"+bankAccount);bankAccountDao.insert(bankAccount);throw new RuntimeException("模拟异常");}
    }

简单解释一下这个测试环境的思路,

  • 两个业务的都是继承同一个业务接口
  • 普通的业务BankAccountService 调用 VIP的业务VIPAccountServiceImpl
  • 通过配置不同的传播方式,来测试事务的传播性

我们在业务A和业务B上分别加入不同的事务注解来

三、测试开始

调用者业务简称A,被调用者的业务简称B

1、默认的REQUIRES

概念:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】

  • 测试场景:业务A上用注解加入声明式事务,业务B则无注解

  • 预测结果:

    • B出现异常 —— AB业务都回滚
    • A出现异常 —— AB都回滚
  • 测试结果:

    • B出现异常 :符合预期

      【学习笔记】Spring事务的传播行为详解

    • A出现异常:符合预期,而且B方法也有执行

      【学习笔记】Spring事务的传播行为详解

结论:这个级别事务会传播给被调用者,并且加入到调用者的事务中。

2、SUPPORTS

踩坑了,第一次测试是有误的,参考文章锤子学习成长日记后解决

概念:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】

  • 测试场景:业务A有声明式事务(传播行为SUPPORTS),B事务待定

  • 结果预测:

    • B加上事务时,B发生异常 —— AB都回滚
    • B加上事务是,A发生异常——AB都回滚
    • B没有事务时,无论A或B发生异常 ,都没有关系
  • 测试结果:

    • B加上事务(默认传播行为) ,B发生异常 —— 【B回滚,A正常】

      【学习笔记】Spring事务的传播行为详解

    • B加上事务(默认传播行为) ,A发生异常 ——【AB都正常没有回滚】

    • B没有事务时:【B出现异常,也不影响A / A出现异常也不影响B】

      【学习笔记】Spring事务的传播行为详解

测试结果完全错误,分析错误原因:概念理解有误,重新更正测试:

简单理解,就是被调用者B在使用了SUPPORTS级别的事务后,在被调用时,会根据调用者是否有启动事务,来判断自己是否启动事务。

重新测试如下:
  • 测试场景:A调用B, A事务待定,B事务传播行为是SUPPORTS

  • 预测结果:

    • A无事务时, B出现异常 —— 不会触发回滚
    • A有事务时,B出现异常 —— AB都回滚
    • A有事务时,A出现异常 ——AB都回滚
  • 测试结果:

    • 符合预期

    【学习笔记】Spring事务的传播行为详解

    • 符合预期

      【学习笔记】Spring事务的传播行为详解

    • 符合预期

结论:被调用者配置了SUPPORTS之后,【调用者有就加入,没有就不管了】

SUPPORTS级别的效果和无声明事务的效果有点类似,是根据调用者的事务声明情况来 配置自己的事务情况的。

所以结论进行一些补充,防止歧义。

3、MANDATORY(强制性的)

概念:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】

  • 测试场景:B配置了MANDATORY级别的事务,A的事务待定

  • 结果预测:

    • 当A没有配置事务时——报错抛出异常——A的业务正常执行/B没有执行
    • 当A配置事务时——B会加入A的事务中
  • 测试结果:

    • 符合预期,抛出异常IllegalTransactionStateException

      【学习笔记】Spring事务的传播行为详解

    • 符合预期,B加入到A的事务中了

结论:如果调用者没有事务则报异常,如果有则加入

4、REQUIRES_NEW

概念:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起

【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】

  • 测试场景:事务A使用REQUIRES_NEW,业务B是否加事务处理待定
  • 结果预测:
    • 业务B有事务—— 开启一个不相干的事务
    • 业务B无事务—— 开启一个不相干的事务
  • 测试结果 :不符合预测
    • 不管B是否有事务,AB都是同一个事务
    • 即: 如果REQUIRES_NEW加在调用者头上,使用的效果和默认的REQUIRES是一样的
重新测试:
  • 测试场景2:业务A是否加事务处理待定, 事务B使用REQUIRES_NEW

  • 结果预测:

    • A不加事务,调用B,A出现异常 —— A异常不影响B事务,AB都成功提交
    • A加事务,调用B,A出现异常 —— A异常不影响B事务,A回滚,B成功提交
    • A加事务,调用B,B出现异常 —— B异常不影响A事务,A提交,B回滚
  • 测试结果:

    • 符合预期:AB都提交,A没有事务所以出现异常也提交成功了,且B成功开启了事务

      【学习笔记】Spring事务的传播行为详解

    • 虽然成功创建的新事务,但是结果却是【AB都回滚了】

      【学习笔记】Spring事务的传播行为详解

      【学习笔记】Spring事务的传播行为详解

    • 符合预期:A成功提交,B回滚了

      【学习笔记】Spring事务的传播行为详解

结论:被调用者配置了REQUIRES_NEW之后,被调用者会单独开启事务。之前的事务就会挂起。

而且一旦被调用者出现异常后,调用者也会当成事务出现异常来进行处理,自然就触发调用者的事务回滚操作。

5、NOT_SUPPORTED

概念:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】

  • 测试场景:业务A在有无事务的情况下,调用NOT_SUPPORTED的事务B

  • 结果预测:

    • 业务A无事务,调用事务B,无论是否有异常,AB都会提交 (都没有事务
    • 业务A有事务,调用事务B —— A出现异常 —— A回滚,B没有事务特性正常提交
    • 业务A有事务,调用事务B —— B出现异常 —— B没有事务特性正常提交,抛出异常会导致A回滚
  • 测试结果:

    • 符合预期

    • A单独回滚了

      【学习笔记】Spring事务的传播行为详解

    • 符合预期,A回滚,B提交

      【学习笔记】Spring事务的传播行为详解

结论:使用了NOT_SUPPORTS的事务,在被调用时会抛弃事务特性;此时调用者会挂起,执行完毕后再恢复。

6、NEVER

概念:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】

简单理解:和MANDATORY是对应的

  • 测试场景:B的传播等级是NEVER,A可以加入事务

  • 结果预测:

    • A有事务时,B报错
    • A无事务时,AB正常按无事务方式执行
  • 测试结果:符合预期:有事务时会报错IllegalTransactionStateException

    【学习笔记】Spring事务的传播行为详解

结论:被调用者使用了该注解后,是一定不支持事务的!一旦调用者支持事务,就会抛出异常

很容易理解,就是我这个方法不使用事务,并且调用我的方法也不允许有事务,如果调用我的方法有事务则我直接抛出异常。

7、NESTED

概念:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】

  • 测试场景:事务B采用NESTD的传播方式

  • 结果预测:

    • 业务A不支持事务,业务B支持一个单独的事务
    • 业务A支持事务,业务A或者B抛出异常,都会导致AB都回滚
    • 业务A支持事务,当业务A使用catch捕获B抛出的异常时,A和B都会呈现单独事务的特性
  • 测试结果:

    • 符合预期
    • 符合预期
    • A出现异常时,AB都会回滚,当B出现异常时,A提交,B回滚
比较难理解的就是这个嵌套关系,和REUIRES和REQUIRES_NEW的区别在哪?
  • 和REQUIRES_NEW的区别

REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。

  • 和REQUIRED的区别

REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响