2.6 Spring事务传播

七种类型

  Spring的事务传播分为七种:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
  默认是第一种,REQUIRED。那么这七种分别是什么意思呢?

  1. REQUIRED:如果存在事务,就连接当前事务,否则就创建事务。
  2. SUPPORTS:如果存在事务,就连接当前事务,否则就没有事务。
  3. MANDATORY:如果存在事务,就连接当前事务,否则就抛出异常。
  4. REQUIRES_NEW:创建新事务,如果存在事务,就挂起当前事务。
  5. NOT_SUPPORTED:无事务执行,如果存在事务,就挂起当前事务。
  6. NEVER:无事务执行,如果存在事务,就抛出异常。
  7. 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();
    }
}

  第一个用例是内层事务成功,外层事务失败,结果如下:
在这里插入图片描述
  将数据库恢复,然后进行下一个实验,结果如下:
在这里插入图片描述
  事务挂起实验,结果符合预期。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值