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

什么是事务的传播行为 Propagetion

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

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

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

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

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

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

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

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

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

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

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

二、枚举类型如下:

image-20230406173550409

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

一、场景描述

  • 场景是一个常见的银行转账场景,数据库的表结构就是用户名+余额即可。
  • 为了测试事务的传播性:我们声明了两个不同的业务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.xsd
                               http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                               http://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;
        }
    
        @Override
        public 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;
    
        @Override
        public 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 {
          
          
        
        @Autowired
        private BankAccountDao bankAccountDao;
    
        @Autowired
        @Qualifier("VIPAccountServiceImpl")
        private BankAccountService bankAccountService;
    
        @Override
        @Transactional
        public 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;
    
        @Override
        public 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出现异常 :符合预期

      image-20230408103510364

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

      image-20230408103833542

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

2、SUPPORTS

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

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

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

  • 结果预测:

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

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

      image-20230408105723980

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

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

      image-20230408104429987

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

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

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

  • 预测结果:

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

    • 符合预期

    image-20230408111921550

    • 符合预期

      image-20230408112043080

    • 符合预期

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

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

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

3、MANDATORY(强制性的)

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

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

  • 结果预测:

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

    • 符合预期,抛出异常IllegalTransactionStateException

      image-20230408120355397

    • 符合预期,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成功开启了事务

      image-20230408130952397

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

      image-20230408131202192

      image-20230408131515998

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

      image-20230408131643671

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

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

5、NOT_SUPPORTED

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

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

  • 结果预测:

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

    • 符合预期

    • A单独回滚了

      image-20230408132632851

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

      image-20230408132521915

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

6、NEVER

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

简单理解:和MANDATORY是对应的

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

  • 结果预测:

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

    image-20230408133309466

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

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

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其异常,这样只有子事务回滚,父事务不受影响

猜你喜欢

转载自blog.csdn.net/Xcong_Zhu/article/details/130028202