什么是事务的传播行为 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.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&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; } @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()); } }
实体类
-
写两个简单的业务类如下
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出现异常 :符合预期
-
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其异常,这样只有子事务回滚,父事务不受影响