模拟一种场景:方法A和B都带有事务注解,其中A调用B,会发生什么? 事务将会如何传递?是合并成一个事务,还是开启另一个新事务呢?这就是事务的传播行为。
REQUIRED
:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
默认的传播行为:只要主方法有事务,调用的方法一定会开启事务,并加入到主方法的事务中
SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
MANDATORY
:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
REQUIRES_NEW
:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起**【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】**
简单理解,只要主方法有事务,调用的方法一定会开启一个新事务,而且是不相干的事务
NOT_SUPPORTED
:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
NEVER
:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
NESTED
:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
创建Spring项目 引入一些必要的依赖
<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.0modelVersion>
<groupId>com.powernodegroupId>
<artifactId>spring6-013-tx-bankartifactId>
<version>1.0-SNAPSHOTversion>
<packaging>jarpackaging>
<dependencies>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-contextartifactId>
<version>5.2.12.RELEASEversion>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.12.RELEASEversion>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.20version>
dependency>
<dependency>
<groupId>org.springframeworkgroupId>
<artifactId>spring-jdbcartifactId>
<version>5.2.7.RELEASEversion>
dependency>
<dependency>
<groupId>com.alibabagroupId>
<artifactId>druidartifactId>
<version>1.2.13version>
dependency>
<dependency>
<groupId>jakarta.annotationgroupId>
<artifactId>jakarta.annotation-apiartifactId>
<version>2.1.1version>
dependency>
<dependency>
<groupId>junitgroupId>
<artifactId>junitartifactId>
<version>4.13.2version>
<scope>testscope>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-coreartifactId>
<version>2.19.0version>
dependency>
<dependency>
<groupId>org.apache.logging.log4jgroupId>
<artifactId>log4j-slf4j2-implartifactId>
<version>2.19.0version>
dependency>
dependencies>
<properties>
<maven.compiler.source>8maven.compiler.source>
<maven.compiler.target>8maven.compiler.target>
properties>
project>
配置文件:
<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="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName"
value="com.mysql.cj.jdbc.Driver">property>
<property name="url"
value="jdbc:mysql://localhost:3306/bank-tx?characterEncoding=utf8&useSSL=false&serverTimezone=UTC&rewriteBatchedStatements=true">property>
<property name="username" value="root">property>
<property name="password" value="root">property>
bean>
<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>
<tx:annotation-driven transaction-manager="txManager"/>
beans>
log4j的配置文件
<configuration>
<loggers>
<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
概念:支持当前事务,如果不存在就新建一个(默认)【没有就新建,有就加入】
测试场景:业务A上用注解加入声明式事务,业务B则无注解
预测结果:
测试结果:
踩坑了,第一次测试是有误的,参考文章锤子学习成长日记后解决
概念:支持当前事务,如果当前没有事务,就以非事务方式执行**【有就加入,没有就不管了】**
测试场景:业务A有声明式事务(传播行为SUPPORTS),B事务待定
结果预测:
测试结果:
简单理解,就是被调用者B在使用了SUPPORTS级别的事务后,在被调用时,会根据调用者是否有启动事务,来判断自己是否启动事务。
测试场景:A调用B, A事务待定,B事务传播行为是SUPPORTS
预测结果:
测试结果:
SUPPORTS级别的效果和无声明事务的效果有点类似,是根据调用者的事务声明情况来 配置自己的事务情况的。
所以结论进行一些补充,防止歧义。
概念:必须运行在一个事务中,如果当前没有事务正在发生,将抛出一个异常**【有就加入,没有就抛异常】**
测试场景:B配置了MANDATORY级别的事务,A的事务待定
结果预测:
测试结果:
概念:开启一个新的事务,如果一个事务已经存在,则将这个存在的事务挂起
【不管有没有,直接开启一个新事务,开启的新事务和之前的事务不存在嵌套关系,之前事务被挂起】
测试场景2:业务A是否加事务处理待定, 事务B使用REQUIRES_NEW
结果预测:
测试结果:
而且一旦被调用者出现异常后,调用者也会当成事务出现异常来进行处理,自然就触发调用者的事务回滚操作。
概念:以非事务方式运行,如果有事务存在,挂起当前事务**【不支持事务,存在就挂起】**
测试场景:业务A在有无事务的情况下,调用NOT_SUPPORTED的事务B
结果预测:
测试结果:
概念:以非事务方式运行,如果有事务存在,抛出异常**【不支持事务,存在就抛异常】**
简单理解:和MANDATORY是对应的
测试场景:B的传播等级是NEVER,A可以加入事务
结果预测:
测试结果:符合预期:有事务时会报错IllegalTransactionStateException
很容易理解,就是我这个方法不使用事务,并且调用我的方法也不允许有事务,如果调用我的方法有事务则我直接抛出异常。
概念:如果当前正有一个事务在进行中,则该方法应当运行在一个嵌套式事务中。被嵌套的事务可以独立于外层事务进行提交或回滚。如果外层事务不存在,行为就像REQUIRED一样。【有事务的话,就在这个事务里再嵌套一个完全独立的事务,嵌套的事务可以独立的提交和回滚。没有事务就和REQUIRED一样。】
测试场景:事务B采用NESTD的传播方式
结果预测:
测试结果:
REQUIRES_NEW是新建一个事务并且新开启的这个事务与原有事务无关,而NESTED则是当前存在事务时(我们把当前事务称之为父事务)会开启一个嵌套事务(称之为一个子事务)。
在NESTED情况下父事务回滚时,子事务也会回滚,而在REQUIRES_NEW情况下,原有事务回滚,不会影响新开启的事务。
REQUIRED情况下,调用方存在事务时,则被调用方和调用方使用同一事务,那么被调用方出现异常时,由于共用一个事务,所以无论调用方是否catch其异常,事务都会回滚
而在NESTED情况下,被调用方发生异常时,调用方可以catch其异常,这样只有子事务回滚,父事务不受影响