七种类型
Spring的事务传播分为七种:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
默认是第一种,REQUIRED。那么这七种分别是什么意思呢?
- REQUIRED:如果存在事务,就连接当前事务,否则就创建事务。
- SUPPORTS:如果存在事务,就连接当前事务,否则就没有事务。
- MANDATORY:如果存在事务,就连接当前事务,否则就抛出异常。
- REQUIRES_NEW:创建新事务,如果存在事务,就挂起当前事务。
- NOT_SUPPORTED:无事务执行,如果存在事务,就挂起当前事务。
- NEVER:无事务执行,如果存在事务,就抛出异常。
- NESTED:如果原事务存在就创建一个嵌套事务。
这七种其实可以分类归纳,可以做个简单的表归纳一下:
传播类型 | 存在当前事务 | 不存在当前事务 |
---|---|---|
REQUIRED | 连接当前事务 | 创建新事务 |
SUPPORTS | 连接当前事务 | 无事务执行 |
MANDATORY | 连接当前事务 | 抛出异常 |
REQUIRES_NEW | 挂起当前事务 | 创建新事务 |
NOT_SUPPORTED | 挂起当前事务 | 无事务执行 |
NEVER | 抛出异常 | 无事务执行 |
NESTED | 嵌套执行 | 创建新事务 |
仔细分析会发现,这7种并没有覆盖所有场景。存在当前事务有连接当前事务、挂起当前事务、抛出异常、嵌套执行四种场景,不存在当前事务有三种场景,总共是 4 × 3 = 12 4\times 3=12 4×3=12种组合,但是只有7种,还有五种没考虑。举个例子,存在事务则抛出异常,不存在事务则创建新事务这种场景就没有定义。但是这种场景没有意义,所以spring的设计者剔除了这五种类型,只保留了这7种。
源码位置
先看存在事务的场景,存在事务的处理入口在spring-tx的AbstractPlatformTransactionManager类中的getTransaction代码中,代码如下:
if (isExistingTransaction(transaction)) {
// Existing transaction found -> check propagation behavior to find out how to behave.
return handleExistingTransaction(def, transaction, debugEnabled);
}
在这个核心方法里,对存在事务的场景下,各个事务传播类型的处理如下:
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NEVER) {
throw new IllegalTransactionStateException(
"Existing transaction found for transaction marked with propagation 'never'");
}
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) {
if (debugEnabled) {
logger.debug("Suspending current transaction");
}
Object suspendedResources = suspend(transaction);
boolean newSynchronization = (getTransactionSynchronization() == SYNCHRONIZATION_ALWAYS);
return prepareTransactionStatus(
definition, null, false, newSynchronization, debugEnabled, suspendedResources);
}
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW) {
if (debugEnabled) {
logger.debug("Suspending current transaction, creating new transaction with name [" +
definition.getName() + "]");
}
SuspendedResourcesHolder suspendedResources = suspend(transaction);
try {
return startTransaction(definition, transaction, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error beginEx) {
resumeAfterBeginException(transaction, suspendedResources, beginEx);
throw beginEx;
}
}
if (definition.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
if (!isNestedTransactionAllowed()) {
throw new NestedTransactionNotSupportedException(
"Transaction manager does not allow nested transactions by default - " +
"specify 'nestedTransactionAllowed' property with value 'true'");
}
if (debugEnabled) {
logger.debug("Creating nested transaction with name [" + definition.getName() + "]");
}
if (useSavepointForNestedTransaction()) {
// Create savepoint within existing Spring-managed transaction,
// through the SavepointManager API implemented by TransactionStatus.
// Usually uses JDBC 3.0 savepoints. Never activates Spring synchronization.
DefaultTransactionStatus status =
prepareTransactionStatus(definition, transaction, false, false, debugEnabled, null);
status.createAndHoldSavepoint();
return status;
}
else {
// Nested transaction through nested begin and commit/rollback calls.
// Usually only for JTA: Spring synchronization might get activated here
// in case of a pre-existing JTA transaction.
return startTransaction(definition, transaction, debugEnabled, null);
}
}
// Assumably PROPAGATION_SUPPORTS or PROPAGATION_REQUIRED.
if (debugEnabled) {
logger.debug("Participating in existing transaction");
}
而对于当前事务不存在,则处理代码在getTransaction方法中:
// No existing transaction found -> check propagation behavior to find out how to proceed.
if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_MANDATORY) {
throw new IllegalTransactionStateException(
"No existing transaction found for transaction marked with propagation 'mandatory'");
}
else if (def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRED ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_REQUIRES_NEW ||
def.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NESTED) {
SuspendedResourcesHolder suspendedResources = suspend(null);
if (debugEnabled) {
logger.debug("Creating new transaction with name [" + def.getName() + "]: " + def);
}
try {
return startTransaction(def, transaction, debugEnabled, suspendedResources);
}
catch (RuntimeException | Error ex) {
resume(null, suspendedResources);
throw ex;
}
}
可见,NEVER、NOT_SUPPORTED和SUPPORTS没有处理代码,也就是默认的无事务执行。
挂起实验
对于这个实验的设计,我的目的是深刻理解挂起。对于挂起,我是没有实践过的,所以必须学习,弄清楚。根据定义,一次只能有一个事务执行,所以只能将旧事务挂起才能执行新事务。事务分两种模式,嵌入模式和扁平模式,在扁平模式下,前一个事务只能挂起。
那么先建好表:
create table account
(
id int auto_increment comment '主键'
primary key,
name varchar(64) null comment '账户名',
balance decimal(16, 2) null comment '余额',
constraint account_name_uindex
unique (name)
)
comment '账户';
INSERT INTO account (name, balance) VALUES ('A', 100.00);
INSERT INTO account (name, balance) VALUES ('B', 100.00);
INSERT INTO account (name, balance) VALUES ('C', 100.00);
INSERT INTO account (name, balance) VALUES ('D', 100.00);
然后搭框架,开始搞。先配置pom.xml:
<?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>
<parent>
<groupId>com.youngthing</groupId>
<artifactId>spring-boot</artifactId>
<version>1.0-SNAPSHOT</version>
</parent>
<groupId>com.youngthing</groupId>
<artifactId>transaction-demo</artifactId>
<version>1.0-SNAPSHOT</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>2.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.1</version>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.25</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
<version>5.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<version>2.7.3</version>
</dependency>
</dependencies>
</project>
开始写Main类:
package net.cloudsun.transationdemo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* 主函数
*
* @author 花书粉丝
* @since 2.0.0
* created in 2022/9/19, 星期一
*/
@MapperScan("net.cloudsun.transationdemo")
@SpringBootApplication
public class Main {
public static void main(String[] args) {
SpringApplication.run(Main.class);
}
}
之后是application.yml文件:
mybatis:
mapper-locations: classpath:mapper/*.xml
spring:
datasource:
url: jdbc:mysql://localhost:3306/learn
driver-class-name: com.mysql.cj.jdbc.Driver
username: root
password: 123456
再是Mapper代码:
package net.cloudsun.transationdemo;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.math.BigDecimal;
/**
* 账户mapper
*
* @author a simple coder
* @since 1.0.0
* created in 2022/9/19, 星期一
*/
@Mapper
public interface AccountMapper {
/**
* 增加余额
* @param accountName 账户名
* @param quantity 数量
* @return
*/
@Update("update account set balance = balance + #{quantity} where name = #{accountName}")
int addBalance(@Param("accountName") String accountName, @Param("quantity") BigDecimal quantity);
}
再是service代码,因为自己调用自己,没有AOP,所以我们需要写两个service,做两层调用,以下是第一个,也是内层的service类:
package net.cloudsun.transationdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
/**
* 转账服务
*
* @author a simple coder
* @since 1.0.0
* created in 2022/9/19, 星期一
*/
@Service
public class TransferService {
@Autowired
private AccountMapper accountMapper;
public void transferAB(boolean success) {
transfer(success, "A", "B");
}
/**
* 挂起事务
*/
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void transferCD(boolean success) {
transfer(success, "C", "D");
}
private void transfer(boolean success, String account1, String account2) {
accountMapper.addBalance(account1, new BigDecimal("-1"));
if (success) {
accountMapper.addBalance(account2, new BigDecimal("1"));
} else {
throw new RuntimeException();
}
}
}
&esmp; 然后是外层的service:
package net.cloudsun.transationdemo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 账户服务
*
* @author a simple coder
* @since 1.0.0
* created in 2022/9/19, 星期一
*/
@Service
public class AccountService {
@Autowired
private TransferService transferService;
@Transactional
public void transferCase1() {
transferService.transferAB(true);
transferService.transferCD(true);
throw new RuntimeException();
}
@Transactional
public void transferCase2() {
transferService.transferAB(true);
try {
transferService.transferCD(false);
} catch (RuntimeException e) {
// 不能影响当前事务
}
}
}
实验结果
&esmp; 先写一个单元测试类:
import net.cloudsun.transationdemo.AccountService;
import net.cloudsun.transationdemo.Main;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* 事务测试
*
* @author a simple coder
* @since 1.0.0
* created in 2022/9/19, 星期一
*/
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = Main.class)
class TransactionTest {
@Autowired
private AccountService accountService;
@Test
void test() {
accountService.transferCase1();
}
@Test
void test2() {
accountService.transferCase2();
}
}
第一个用例是内层事务成功,外层事务失败,结果如下:
将数据库恢复,然后进行下一个实验,结果如下:
事务挂起实验,结果符合预期。