拿下 Spring 事务

 2023-01-10
原文作者:程序员小杰 原文地址:https://juejin.cn/post/7123191905839480839

日积月累,水滴石穿 ?

什么是事务

事务是数据库操作的最基本单元,是逻辑上的一组操作,要么都成功,要么都失败;是一个不可分割的工作单元。

事务的特性

事务具有 4 个特性:原子性、一致性、隔离性、持久性,简称为 ACID。

  • 原子性(Atomicity):一个事务是一个不可分割的工作单位, 一个事务中 包括的 操作要么都成功要么都失败
  • 一致性(Consistency):事务必须保证数据库从一个一致性状态变到另一个一致性状态。比如转账的总金额,不能转着转着总金额少了或者多了;再比如转账金额需要小于等于余额。大部分一致性的需求需要程序员写业务代码保证。
  • 隔离性(Isolation):一个事务的执行不能被其它事务干扰,即一个事务内部的操作及使用的数据对并发的其它事务是隔离的,并发执行的各个事务之间不能互相打扰。
  • 持久性(Durability):指 一个事务一旦提交 ,它对数据库中数据的改变就是永久性的,后面的其它操作和故障都不应该对其有任何影响。

为什么要用Spring事务

在日常开发中,事务涉及到的场景是非常多的,我们常常在一个 service 中需要调用不同的 dao 层方法,那我们就需要保证这些方法要么同时成功要么同时失败。

举例:银行转账。小明给小红转 100 元。小明的账户需要减少余额 100,小红的账户需要增加余额 100。这是两个操作,也是两个事务,需要一起成功或者一起失败。如果在小明转账成功之后发生了异常,就会出现小明的账户减 100 余额,但是小红的账户并没有加 100 余额。就会造成钱丢失的情况。这是绝对不允许的。所以我们需要 使用Spring事务 保证多个操作同时成功或者同时失败,伪代码如下:

    public void accountMoney() {
        int money = 100;
        //小明 少 100
        userDao.reduceMoney(money);
        // 其他业务 发生异常
        int i = 1/0;
        //小红余额没有发生变化
        userDao.addMoney(money);
    }

Spring事务管理方式

Spring 支持 2 种事务管理方式。

  • 编程式事务管理 编程式事务管理是通过编写代码实现的事务管理。可以根据需求规定事务从哪里开始,到哪里结束,拥有很高的灵活性。但是这种方式,会使业务代码与事务规则高度耦合,难以维护,因此我们很少使用这种方式对事务进行管理。所以,本文给大家介绍的是如何使用声明式事务管理。
  • 声明式事务管理 声明式事务管理可以通过 2 种方式实现,分别是 XML和注解方式。Spring 在进行声明式事务管理时,底层使用了 AOP 。

事务管理器

Spring 并不会直接管理事务,而是通过事务管理器对事务进行管理的。

PlatformTransactionManager

Spring 提供了一个 PlatformTransactionManager 接口,这个接口被称为 Spring 的事务管理器,其源码如下:

    public interface PlatformTransactionManager {
    // 根据传入的 TransactionDefinition 对象获取一个事务状态对象
       TransactionStatus getTransaction(@Nullable TransactionDefinition var1) throws TransactionException;
    
    // 提交事务
       void commit(TransactionStatus var1) throws TransactionException;
       
    // 回滚事务
       void rollback(TransactionStatus var1) throws TransactionException;
    }

该接口的源码很简单。这些接口针对不同的框架提供了不同的实现类,如下:

实现类 说明
org.springframework.jdbc.datasource.DataSourceTransactionManager 提供给SpringJDBC、MBatis的事务管理器
org.springframework.orm.hibernate5.HibernateTransactionManager 提供给Hibernate的事务管理器
org.springframework.orm.jpa.JpaTransactionManager 提供给JPA的事务管理器
org.springframework.jdo.JdoTransactionManager 提供给Jdo的事务管理器
org.springframework.transaction.jta.JtaTransactionManager 提供给JTA的事务管理器

注意:这些实现类,需要导入对应的依赖才能看到。 该接口中还有两个对象,分别是 TransactionDefinitionTransactionStatus

TransactionDefinition

  • TransactionDefinition:事务定义,定义了事务的名称,传播属性,事务隔离级别,是否只读,超时时间。
    public interface TransactionDefinition {
    
        int PROPAGATION_REQUIRED = 0;
        int PROPAGATION_SUPPORTS = 1;
        int PROPAGATION_MANDATORY = 2;
        int PROPAGATION_REQUIRES_NEW = 3;
        int PROPAGATION_NOT_SUPPORTED = 4;
        int PROPAGATION_NEVER = 5;
        int PROPAGATION_NESTED = 6;
        
        int ISOLATION_DEFAULT = -1;
        int ISOLATION_READ_UNCOMMITTED = 1;
        int ISOLATION_READ_COMMITTED = 2;
        int ISOLATION_REPEATABLE_READ = 4;
        int ISOLATION_SERIALIZABLE = 8;
        
        int TIMEOUT_DEFAULT = -1;
    
        default int getPropagationBehavior() {
            return 0;
        }
    
        default int getIsolationLevel() {
            return -1;
        }
    
        default int getTimeout() {
            return -1;
        }
    
        default boolean isReadOnly() {
            return false;
        }
    
        @Nullable
        default String getName() {
            return null;
        }
    
        static TransactionDefinition withDefaults() {
            return StaticTransactionDefinition.INSTANCE;
        }
    }
  • PROPAGATION_** 0 ~ 7 代表的是事务传播行为
  • ISOLATION_** -1 ~ 8 代表的是事务的隔离级别
  • TIMEOUT_DEFAULT 默认的超时时间,-1,代表使用数据库的超时时间
  • getPropagationBehavior:获取事务的传播行为,默认为 PROPAGATION_REQUIRED
  • getIsolationLevel:获取事务的隔离级别,默认为所使用数据库的隔离级别
  • getTimeout:获取事务的超时时间
  • isReadOnly:事务是否只读
  • getName:获取事务的名称

TransactionStatus

  • TransactionStatus:事务状态,保存了事务执行过程中的状态。
    public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
    
    	
    	boolean hasSavepoint();
    
    	
    	@Override
    	void flush();
    
    }

方法说明如下:

名称 说明
hasSavepoint 事务内部是否带有保存点
flush 将底层会话刷新到数据存储,如Hibernate/JPA的会话。是否生效由具体事务资源实现决定。例如JDBC类型的事务就无任何影响。
  • TransactionExecution
    public interface TransactionExecution {
    
       boolean isNewTransaction();
    
       void setRollbackOnly();
    
       boolean isRollbackOnly();
    
      
       boolean isCompleted();
    
    }

方法说明如下:

名称 说明
isNewTransaction 当前事务是否是新的
setRollbackOnly 设置事务回滚
isRollbackOnly 事务是否已被标记为回滚
isCompleted 事务是否完成,即是否已经提交或回滚
  • SavepointManager
    public interface SavepointManager {
    
    	Object createSavepoint() throws TransactionException;
    	
    	void rollbackToSavepoint(Object savepoint) throws TransactionException;
    
    	void releaseSavepoint(Object savepoint) throws TransactionException;
    
    }

方法说明如下:

名称 说明
createSavepoint 创建保存点
rollbackToSavepoint 回滚到给定的保存点
releaseSavepoint 释放给定的保存点

TransactionStatus 该对象默认的底层实现为 DefaultTransactionStatus

事务传播行为

事务传播行为指的是,多事务方法之间进行调用时,这个过程中事务应该如何进行管理。例如,事务方法 A 在调用事务方法 B 时,B 方法是在调用者 A 方法的事务中运行呢,还是为自己开启一个新事务运行,这就是由事务方法 B 的事务传播行为决定的。

事务方法:能让数据库表数据发生改变的方法,例如新增、删除、修改数据的方法。

    public enum Propagation {
        REQUIRED(0),
        SUPPORTS(1),
        MANDATORY(2),
        REQUIRES_NEW(3),
        NOT_SUPPORTED(4),
        NEVER(5),
        NESTED(6);
    }
行为 说明
REQUIRED 如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行
SUPPORTS 如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。
MANDATORY 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
REQUIRES_NEW 当前的方法必须启动新事务,并在它自己的事务内运行,如果有事务正在运行,应该将它挂起
NOT_SUPPORTED 以非事务方式运行,如果当前存在事务,则把当前事务挂起。
NEVER 以非事务方式运行,如果当前存在事务,则抛出异常。
NESTED 如果当前存在事务,则创建一个新事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于REQUIRED。

根据上面的描述,我们可以将行为分为三大类。

  • 不要事务:NEVER、NOT_SUPPORTED。
  • 如果有则用:SUPPORTS
  • 必须使用事务:REQUIRED、REQUIRES_NEW、NESTED、MANDATORY

隔离级别

事务有一个特性为隔离性,多事务操作之间不会产生影响。但如果不考虑隔离性,则会产生三个读问题:脏读、不可重复读、虚(幻)读。

  • 脏读:一个未提交事务读取到另一个未提交事务的数据
  • 不可重复读:一个未提交事务读取到另一个提交事务修改的数据
  • 虚(幻)读:一个未提交事务读取到另一提交事务添加的数据

那如何解决呢?可以通过设置事务隔离级别,解决读问题!Spring 中提供了隔离级别的枚举类Isolation,可以设置数据库连接的隔离级别。

级别 说明
DEFAULT 使用所用的数据库的隔离级别
READ_UNCOMMITTED(读未提交) 可以读取到尚未提交的更改,可能导致脏读、幻读和不可重复读
READ_COMMITTED(读已提交) Oracle的默认级别,可以读取到已提交的更改的数据,防止脏读,可能出现幻读和不可重复读
REPEATABLE_READ(可重复读) MySQL的默认级别,同一条SQL多次执行,可以读取到已提交的新增的数据,防止脏读和不可重复读,可能出现幻读
SERIALIZABLE 可串行化,什么读问题都不会产生

加入依赖

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.8.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-tx</artifactId>
        <version>5.0.8.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.0.8.RELEASE</version>
    </dependency>
    
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjrt</artifactId>
        <version>1.9.6</version>
    </dependency>
    
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.6</version>
    </dependency>

xml方式

我们先来看看不使用事务会发生什么情况。创建名为 aopxml的包。

提供数据库脚本

    CREATE TABLE `tx_test` (
      `id` int(11) NOT NULL,
      `name` varchar(64) DEFAULT NULL,
      `money` decimal(10,0) DEFAULT NULL,
      PRIMARY KEY (`id`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
    
    
    INSERT INTO `tx_test`(`id`, `name`, `money`) VALUES (1, '张三', 1000);
    INSERT INTO `tx_test`(`id`, `name`, `money`) VALUES (2, '李四', 1000);

开发代码

新建 dao 包

在类中提供两个方法,一个张三增加金额,一个李四减金额。

    @Repository
    public class TXDao {
    
        @Autowired
        private JdbcTemplate jdbcTemplate;
    
        /**
         * 给张三增加金额
         */
        public void add(){
            String sql = "update `tx_test`  set money = money + 100 where id = 1;";
            jdbcTemplate.update(sql);
        }
    
        /**
         * 给李四减金额
         */
        public void reduce(){
            String sql = "update `tx_test`  set money = money - 100 where id = 2;";
            jdbcTemplate.update(sql);
        }
    }
新建 entity
    public class TxTest {
    
        private Integer id;
    
        private String name;
    
        private BigDecimal money;
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public BigDecimal getMoney() {
            return money;
        }
    
        public void setMoney(BigDecimal money) {
            this.money = money;
        }
    }
新建 service 包
    @Service
    public class TXServiceImpl {
    
        @Autowired
        private TXDao tx;
    
        public void transfer(){
            tx.add();
            int i = 1/0;
            tx.reduce();
        }
    }

项目结构如下:

202301012015257121.png

测试
    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("tx.xml");
        TXServiceImpl bean = context.getBean(TXServiceImpl.class);
        bean.transfer();
    }

控制台出现异常

    Exception in thread "main" java.lang.ArithmeticException: / by zero
    	at cn.cxyxj.txannon.service.TestServiceImpl.transfer(TestServiceImpl.java:20)
    	at cn.cxyxj.txannon.AppMain.main(AppMain.java:20)

202301012015261952.png

再来查看数据库数据,可以发现张三的金额增加了,但是李四的金额没有减。银行哭死!!! 所以我们需要引入 Spring 事务,解决上述出现的问题。

引入 tx 命名空间

    <?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:aop="http://www.springframework.org/schema/aop"
           xmlns:tx="http://www.springframework.org/schema/tx" 
           xmlns:context="http://www.springframework.org/schema/context"
           xsi:schemaLocation="http://www.springframework.org/schema/beans
           http://www.springframework.org/schema/beans/spring-beans.xsd
           http://www.springframework.org/schema/aop
           https://www.springframework.org/schema/aop/spring-aop.xsd
           http://www.springframework.org/schema/tx
           http://www.springframework.org/schema/tx/spring-tx.xsd 
           http://www.springframework.org/schema/context 
           http://www.springframework.org/schema/context/spring-context.xsd">
    
    
    </beans>

注意: 上面说过 Spring 提供的声明式事务管理是依赖于 Spring AOP 实现的,因此还需要添加 aop 命名空间配置。当然我还额外引入了 spring-context 命名空间。

配置事务管理器以及 JdbcTemplate

    <!--引入 jdbc.properties 中的配置-->
        <context:property-placeholder location="classpath:jdbc.properties">
        </context:property-placeholder>
    
        <!--配置数据源 -->
        <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
            <!--数据库连接地址-->
            <property name="url" value="${jdbc.url}"/>
            <!--数据库的用户名-->
            <property name="username" value="${jdbc.username}"/>
            <!--数据库的密码-->
            <property name="password" value="${jdbc.password}"/>
            <!--数据库驱动-->
            <property name="driverClassName" value="${jdbc.driver}"/>
        </bean>
        
        <!--定义 JdbcTemplate Bean-->
        <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
            <!--将数据源的 Bean 注入到 JdbcTemplate 中-->
            <property name="dataSource" ref="dataSource"></property>
        </bean>
    
        <!--配置事务管理器,以 JDBC 为例-->
        <bean id="transactionManager"
              class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
            <property name="dataSource" ref="dataSource"></property>
        </bean>

配置的事务管理器实现为 DataSourceTransactionManager,是 JDBC 和 MBatis 的PlatformTransactionManager 接口实现。

jdbc.properties

    jdbc.driver=com.mysql.jdbc.Driver
    jdbc.url=jdbc:mysql://127.0.0.1/tx_test?useSSL=false
    jdbc.username=xxxx
    jdbc.password=root
    # 打印日志
    logging.level.root=debug

配置事务通知

配置事务通知,指定所需要使用的事务管理器以及指定事务作用的方法和该事务属性。

     <!--配置通知-->
        <tx:advice id="txAdvice" transaction-manager="transactionManager">
            <!--配置事务参数-->
            <tx:attributes>
                <!--指定哪个方法上面添加事务-->
                <tx:method name="transfer*" propagation="REQUIRED" isolation="DEFAULT" read-only="false" timeout="10"/>
                <!--可以配置多个方法 <tx:method name="account*"/>-->
            </tx:attributes>
        </tx:advice>

transaction-manage参数的默认值就是 transactionManager,如果事务管理器 id 与其一致,则可以不用指定。 <tx:method>元素包含多个属性参数,可以为某个或某些方法(name 属性指定的方法)定义事务属性,如下表所示:

事务属性 说明
propagation 指定事务的传播行为,默认为REQUIRED
isolation 指定事务的隔离级别,默认为所使用数据库的隔离级别
read-only 指定是否为只读事务,默认为false
timeout 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为-1,代表不超时
rollback-for 指定出现哪些异常进行事务回滚
no-rollback-for 指定出现哪些异常不进行事务回滚
配置切入点和切面
    <aop:config>
        <!--配置切入点-->
        <aop:pointcut id="pt" expression="execution(*
    com.cxyxj.aopxml.service.TXServiceImpl.*(..))"/>
        <!--配置切面-->
        <aop:advisor advice-ref="txAdvice" pointcut-ref="pt"/>
    </aop:config>

如上写法就对 transfer 方法进行了事务管理。就不会出现小明减少余额,而小红没有增加余额的情况,发生了异常就进行回滚。

注解方式

使用注解方式就不会有上面如此琐碎的配置了。再重新创建名为txannon包,将 xml 方式使用到的 entity、dao、service 相关代码 copy 过来。

开启事务

使用 EnableTransactionManagement注解开启事务;相当于tx:annotation-driven 标签。如果是使用 Spring Boot,则该注解都不需要手动加。

    @ComponentScan(basePackages = "com.cxyxj.txannon")
    @EnableTransactionManagement //开启事务
    public class AppMain {
    
    }

创建配置类

如果是使用 Spring Boot,则该配置类都不需要,使用其 Spring Boot提供的默认配置。

    @Configuration
    @PropertySource("jdbc.properties")
    public class TxConfig {
    
        @Value("${jdbc.url}")
        private String url;
    
        @Value("${jdbc.username}")
        private String username;
    
        @Value("${jdbc.password}")
        private String password;
    
        @Value("${jdbc.driver}")
        private String driverClassName;
    
        //创建数据库连接池
        @Bean
        public DriverManagerDataSource getDruidDataSource() {
            DriverManagerDataSource dataSource = new DriverManagerDataSource();
            dataSource.setDriverClassName(driverClassName);
            dataSource.setUrl(url);
            dataSource.setUsername(username);
            dataSource.setPassword(password);
            return dataSource;
        }
    
        //创建 JdbcTemplate 对象
        @Bean
        public JdbcTemplate getJdbcTemplate(DataSource dataSource) {
            //到 ioc 容器中根据类型找到 dataSource
            JdbcTemplate jdbcTemplate = new JdbcTemplate();
            //注入 dataSource
            jdbcTemplate.setDataSource(dataSource);
            return jdbcTemplate;
        }
        
        //创建事务管理器
        @Bean
        public DataSourceTransactionManager
        getDataSourceTransactionManager(DataSource dataSource) {
            DataSourceTransactionManager transactionManager = new
                    DataSourceTransactionManager();
            transactionManager.setDataSource(dataSource);
            return transactionManager;
        }
    
    }

可以不需要在配置切点(PointCut)和切面(Advisor)了。

添加事务注解

在需要添加事务的方法上添加 @Transactional注解,表明该方法需要进行事务管理。

    @Service
    public class TXServiceImpl {
    
        @Autowired
        private TXDao tx;
    
        @Transactional
        public void transfer(){
            tx.add();
            int i = 1/0;
            tx.reduce();
        }
    }

@Transactional这个注解可以添加到类上面,也可以添加方法上面。如果把这个注解添加到类上面,这个类里面所有的方法都添加事务,如果把这个注解添加方法上面,则是为这个方法添加事务。

@Transactional

Transactional 这个注解里面可以配置很多事务相关参数。

    public @interface Transactional {
    
        @AliasFor("transactionManager")
        String value() default "";
    
        @AliasFor("value")
        String transactionManager() default "";
    
        Propagation propagation() default Propagation.REQUIRED;
    
        Isolation isolation() default Isolation.DEFAULT;
    
        int timeout() default -1;
    
        boolean readOnly() default false;
    
        Class<? extends Throwable>[] rollbackFor() default {};
    
        String[] rollbackForClassName() default {};
    
        Class<? extends Throwable>[] noRollbackFor() default {};
    
        String[] noRollbackForClassName() default {};
    }
事务属性 说明
value 指定事务管理器,默认为transactionManager。
transactionManager 跟value一致。
propagation 指定事务的传播行为,默认为REQUIRED
isolation 指定事务的隔离级别,默认为所使用数据库的隔离级别
read-only 指定是否为只读事务,默认为false
timeout 表示超时时间,单位为“秒”。事务在指定的超时时间后,自动回滚。避免事务长时间不提交导致数据库资源占用。默认为-1,代表不超时
rollbackFor 指定出现哪些异常进行事务回滚
rollbackForClassName 指定异常类名称,进行事务回滚
noRollbackFor 指定出现哪些异常不进行事务回滚
noRollbackForClassName 指定出现哪些异常类名称不进行事务回滚

基本用法会了,现在就来看看事务的传播行为,这是 Spring 事务中难以理解的一块,因为它的场景很多。

事务传播行为详解

REQUIRED

如果有事务在运行,当前的方法就在这个事务内运行,否则,就启动一个新的事务,并在自己的事务内运行。

202301012015266223.png

  • 如果 transfer方法没有事务,则 reduce方法会创建一个事务。
  • 由于两个方法的事务的传播行为都为 Propagation.REQUIRED。所以 transfer方法会先开启一个事务,而 reduceRequired会加入到 transfer方法的事务中,这两个方法用的是同一个事务,所以不论是在哪个方法中抛出异常,所有操作都会回滚。

思考 1:

202301012015271144.png 对 reduce 方法抛出的异常,进行 try、catch。那事务如何执行呢?提交、回滚。部分提交,部分回滚?

思考2:

202301012015275875.pngreduce方法的 public 修改为 private ,并且将 @Transactional注解注释,会出现什么情况呢?

思考 3:

202301012015280986.png reduceRequired 方法不使用代理对象进行调用,想想事务是否还会生效。

思考4:

202301012015288237.png

reduce方法的 public 修改为 private ,并且将 @Transactional注解注释,并对 reduce 方法抛出的异常进行 try、catch。现在事务如何执行呢?提交、回滚。部分提交,部分回滚?

REQUIRES_NEW

当前方法必须启动新事务,并在它自己的事务内运行。如果有事务正在运行,应该将它挂起。

202301012015295158.png reduce 方法行为修改为 Propagation.REQUIRES_NEW。transfer 方法创建事务,然后调用 reduce 方法,reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。所以在该例子中会创建两个事务。由于有两个事务,那事务的回滚时就出现了几种情况。

  • 场景一

202301012015299719.png transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚。

  • 场景二

    2023010120153107610.png 两个方法的操作都会回滚。这是由于 reduce 方法的异常会向 transfer 方法传递。

  • 场景三

2023010120153173311.png transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。

  • 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。
  • 如果 transfer 方法有事务,则 reduce 方法会将 transfer 方法的事务挂起,并创建属于 reduce 方法的事务。如果此时 transfer 方法发生了异常,则 transfer 方法操作会回滚,但不会导致 reduce 方法回滚。如果 reduce 方法发生了异常,则 reduce 方法操作会回滚,如果 transfer 方法没有捕获 reduce 方法的异常,那 transfer 方法也会回滚。

NESTED

如果当前存在事务(主事务),则创建一个新事务作为当前事务的嵌套事务(子事务,底层是创建了一个保存点)来运行;如果当前没有事务,则该取值等价于 REQUIRED

2023010120153382312.png

  • 如果 transfer 方法没有事务,则 reduce 方法会创建一个事务。
  • 如果 transfer 方法有事务,则 reduce 方法会创建一个新事务,作为 transfer 方法事务的嵌套事务来运行。那会有什么场景呢?
  • 场景一

2023010120153452913.png transfer 方法发生异常并回滚,会导致 reduce 方法 同时回滚。

  • 场景二

2023010120153536014.png

transfer 方法进行的操作不会回滚,reduce 方法的操作会回滚。 注意 :transfer 方法需要进行 catch,不然 transfer 方法也会回滚。

主事务方法异常回滚时,会同时回滚子事务。而子事务可以单独异常回滚,可以不影响主事务和其他子事务(前提是需要处理掉子事务的异常)

MANDATORY

如果 当前存在事务,则加入该事务 ;如果当前没有事务,则抛出异常。

2023010120153644515.png

由于 transfer 方法没有事务,在启动时就会抛出异常,如下:

    No existing transaction found for transaction marked with propagation 'mandatory'

SUPPORTS

如果有事务在运行,当前的方法就在这个事务内运行;如果当前没有事务,则以非事务的方式运行。

2023010120153725316.png

由于 transfer 方法没有事务,所以 reduce 方法也不会创建事务。就会 从数据源获取JDBC连接 ,即使发生了异常,数据还是修改成功。

NOT_SUPPORTED

以非事务方式运行,如果当前存在事务,则把当前事务挂起。

2023010120153795917.png

transfer 方法有事务,但 reduce 方法传播行为是 NOT_SUPPORTED,所以会将 transfer 方法事务挂起,reduce 方法以非事务的方式运行,会 从数据源重新获取JDBC连接

所以图片例子会出现 transfer 方法进行的操作会回滚,reduce 方法的操作不会回滚,并且对数据库数据修改成功。

NEVER

以非事务方式运行,如果当前存在事务,则抛出异常。

2023010120153852718.png

由于 transfer 方法有事务,在启动时就会抛出异常,如下:

    Existing transaction found for transaction marked with propagation 'never'

回滚规则

上面一直在说遇到异常就回滚,那是遇到所有异常都会回滚吗?不是的,默认情况下,Spring 事务只有遇到 RuntimeException 以及 Error 时才会回滚,在遇到检查型异常时是不会回滚的,比如 IOException、TimeoutException

那如果想在发生检查型异常时也进行回滚呢,我们可以使用 rollbackFor 属性进行如下配置:

2023010120153927219.png

那同理,如果遇到某个异常,不想进行回滚,使用 noRollbackFor 属性配置如下:

2023010120153986220.png

  • 如你对本文有疑问或本文有错误之处,欢迎评论留言指出。如觉得本文对你有所帮助,欢迎点赞 + 收藏。