【学习笔记】Spring事务的传播行为详解
什么是事务的传播行为 Propagetion
模拟一种场景:方法A和B都带有事务注解,其中A调用B,会发生什么? 事务将会如何传递?是合并成一个事务,还是开启另一个新事务呢?这就是事务的传播行为。
一、Spring定义了一个枚举,一共有七种传播行为:
-
REQUIRED
:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】默认的传播行为:只要主方法有事务,调用的方法一定会开启事务,并加入到主方法的事务中
-
SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】 -
MANDATORY
:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】 -
REQUIRES_NEW
:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】简单理解,只要主方法有事务,调用的方法一定会开启一个新事务,而且是不相干的事务
-
NOT_SUPPORTED
:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】 -
NEVER
:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】 -
NESTED
:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
二、枚举类型如下:
用实际的场景来模拟事务的传播行为
一、场景描述
- 场景是一个常见的银行转账场景,数据库的表结构就是用户名+余额即可。
- 为了测试事务的传播性:我们声明了两个不同的业务A和B
- A调用了B,且A已经配置了Spring的事务
- 为了便于测试,省略了表现层
二、搭建测试环境
-
创建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&useSSL=false&serverTimezone=UTC&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>
-
创建数据库。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;
-
写简单的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());} }
实体类
-
写两个简单的业务类如下
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出现异常 :符合预期
-
A出现异常:符合预期,而且B方法也有执行
-
结论:这个级别事务会传播给被调用者,并且加入到调用者的事务中。
2、SUPPORTS
踩坑了,第一次测试是有误的,参考文章锤子学习成长日记后解决
概念:支持当前事务,如果当前没有事务,就以非事务方式执行【有就加入,没有就不管了】
-
测试场景:业务A有声明式事务(传播行为SUPPORTS),B事务待定
-
结果预测:
- B加上事务时,B发生异常 —— AB都回滚
- B加上事务是,A发生异常——AB都回滚
- B没有事务时,无论A或B发生异常 ,都没有关系
-
测试结果:
-
B加上事务(默认传播行为) ,B发生异常 —— 【B回滚,A正常】
-
B加上事务(默认传播行为) ,A发生异常 ——【AB都正常没有回滚】
-
B没有事务时:【B出现异常,也不影响A / A出现异常也不影响B】
-
测试结果完全错误,分析错误原因:概念理解有误,重新更正测试:
简单理解,就是被调用者B在使用了SUPPORTS级别的事务后,在被调用时,会根据调用者是否有启动事务,来判断自己是否启动事务。
重新测试如下:
-
测试场景:A调用B, A事务待定,B事务传播行为是SUPPORTS
-
预测结果:
- A无事务时, B出现异常 —— 不会触发回滚
- A有事务时,B出现异常 —— AB都回滚
- A有事务时,A出现异常 ——AB都回滚
-
测试结果:
- 符合预期
-
符合预期
-
符合预期
结论:被调用者配置了SUPPORTS之后,【调用者有就加入,没有就不管了】
SUPPORTS级别的效果和无声明事务的效果有点类似,是根据调用者的事务声明情况来 配置自己的事务情况的。
所以结论进行一些补充,防止歧义。
3、MANDATORY(强制性的)
概念:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常【有就加入,没有就抛异常】
-
测试场景:B配置了MANDATORY级别的事务,A的事务待定
-
结果预测:
- 当A没有配置事务时——报错抛出异常——A的业务正常执行/B没有执行
- 当A配置事务时——B会加入A的事务中
-
测试结果:
-
符合预期,抛出异常IllegalTransactionStateException
-
符合预期,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成功开启了事务
-
虽然成功创建的新事务,但是结果却是【AB都回滚了】
-
符合预期:A成功提交,B回滚了
-
结论:被调用者配置了REQUIRES_NEW之后,被调用者会单独开启事务。之前的事务就会挂起。
而且一旦被调用者出现异常后,调用者也会当成事务出现异常来进行处理,自然就触发调用者的事务回滚操作。
5、NOT_SUPPORTED
概念:以非事务方式运行,如果有事务存在,挂起当前事务【不支持事务,存在就挂起】
-
测试场景:业务A在有无事务的情况下,调用NOT_SUPPORTED的事务B
-
结果预测:
- 业务A无事务,调用事务B,无论是否有异常,AB都会提交 (都没有事务
- 业务A有事务,调用事务B —— A出现异常 —— A回滚,B没有事务特性正常提交
- 业务A有事务,调用事务B —— B出现异常 —— B没有事务特性正常提交,抛出异常会导致A回滚
-
测试结果:
-
符合预期
-
A单独回滚了
-
符合预期,A回滚,B提交
-
结论:使用了NOT_SUPPORTS的事务,在被调用时会抛弃事务特性;此时调用者会挂起,执行完毕后再恢复。
6、NEVER
概念:以非事务方式运行,如果有事务存在,抛出异常【不支持事务,存在就抛异常】
简单理解:和MANDATORY是对应的
-
测试场景:B的传播等级是NEVER,A可以加入事务
-
结果预测:
- A有事务时,B报错
- A无事务时,AB正常按无事务方式执行
-
测试结果:符合预期:有事务时会报错
IllegalTransactionStateException
结论:被调用者使用了该注解后,是一定不支持事务的!一旦调用者支持事务,就会抛出异常
很容易理解,就是我这个方法不使用事务,并且调用我的方法也不允许有事务,如果调用我的方法有事务则我直接抛出异常。
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其异常,这样只有子事务回滚,父事务不受影响