Spring 的事务传播机制

一、什么是事务传播机制?

  事务传播机制,顾名思义就是多个事务方法之间调用,事务如何在这些方法之间传播,是重新创建事务还是使用父方法的事务,父方法的回滚对子方法的事务是否有影响等等,这些都是事务传播机制决定的。

二、Spring 事务传播特性有哪些?

  多个事务方法相互调用时,事务如何在这些方法之间进行传播呢?Spring 提供了 7 种不同的传播特性,来保证事务的正常进行。源码如下所示。

package org.springframework.transaction.annotation;

public enum Propagation {
    
    
    REQUIRED(0),
    SUPPORTS(1),
    MANDATORY(2),
    REQUIRES_NEW(3),
    NOT_SUPPORTED(4),
    NEVER(5),
    NESTED(6);

    private final int value;

    private Propagation(int value) {
    
    
        this.value = value;
    }

    public int value() {
    
    
        return this.value;
    }
}

  Propagation(传播特性)有以下选项可供使用:REQUIRED、SUPPORTS、MANDATORY、REQUIRES_NEW、NOT_SUPPORTED、NEVER、NESTED。
  在陈述传播特性之前,以如下伪代码为例,陈述几个术语。

ServiceA.methodA(required){
    
    
	// methodA 方法的业务
	ServiceB.methodB(required);
}

  ServiceA 中的 methodA 方法调用 ServiceB 中的 methodB 方法,ServiceA.methodA 方法的事务被称为 “当前事务”(current transaction,有些文章中也称为父事务或外部事务),ServiceB.methodB 方法的事务被称为子事务(或内部事务)。上述伪代码设置了当前事务传播特性为 REQUIRED,子事务传播特性为 REQUIRED。
  Spring 的几种事务传播特性含义如下(均站在子方法的立场而言,即上述伪代码中的 methodB)。

  • REQUIRED
    这是 Spring 默认的传播特性。如果当前没有事务,则新建一个事务;如果当前存在事务,则加入这个事务。这是实际项目开发中最常见的配置。
    例如,ServiceA.methodA 的事务传播特性定义为 Propagation.REQUIRED,ServiceB.methodB 的事务传播特性定义为 Propagation.REQUIRED,那么由于执行 ServiceA.methodA 的时候,ServiceA.methodA 已经起了事务,这时调用 ServiceB.methodB,ServiceB.methodB 看到自己已经运行在 ServiceA.methodA 的方法内部,就不再起新的事务,而是加入当前事务。这样,在 ServiceA.methodA 或者在 ServiceB.methodB 内的任何地方出现异常,事务都会被回滚,即使 ServiceB.methodB 的事务已经被提交,但是 ServiceA.methodA 在接下来异常要回滚,ServiceB.methodB 也要回滚。

  • SUPPORTS
    如果存在当前事务,即以事务的形式运行,如果当前没有事务,则以非事务的方式执行。

  • MANDATORY
    存在当前事务,则加入当前事务,如果没有当前事务,就抛出异常。
    必须在一个事务中运行,也就是说,它只能被一个父事务调用,否则子方法就抛出异常。

  • REQUIRES_NEW
    创建一个新事务,如果存在当前事务,则把当前事务挂起。例如,我们设计 ServiceA.methodA 的事务传播特性为 Propagation.REQUIRED,ServiceB.methodB 的事务传播特性为 Propagation.REQUIRES_NEW,那么当执行到 ServiceB.methodB 的时候,ServiceA.methodA 所在的事务就会挂起,ServiceB.methodB 会起一个新的事务,等到 ServiceB.methodB 的事务完成以后,ServiceA.methodA 所在的事务才继续执行。它与 REQUIRED 的事务区别在于事务的回滚程度。因为 ServiceB.methodB 是新起一个事务,相当于存在两个不同的事务。如果 ServiceB.methodB 已经提交,那么 ServiceA.methodA 失败回滚,ServiceB.methodB 是不会回滚的。如果 ServiceB.methodB 失败回滚,如果它抛出的异常被 ServiceA.methodA 捕获,ServiceA.methodA 事务仍然可能提交。

  • NOT_SUPPORTED
    以非事务方式执行操作,如果存在当前事务,就把当前事务挂起。例如,ServiceA.methodA 的事务传播特性是 Propagation.REQUIRED,而 ServiceB.methodB 的事务传播特性是 Propagation.NOT_SUPPORTED,那么当执行到 ServiceB.methodB 时,ServiceA.methodA 的事务挂起,而 ServiceB.methodB 以非事务的状态运行完,再继续 ServiceA.methodA 的事务。

  • NEVER
    不使用事务,以非事务方式执行。如果当前事务存在,则抛出异常,不能在事务中运行。假设 ServiceA.methodA 的事务传播特性是 Propagation.REQUIRED,而 ServiceB.methodB 的事务传播特性是 Propagation.NEVER ,那么 ServiceB.methodB 就要抛出异常了。

  • NESTED
    如果当前事务存在,就运行一个嵌套事务。如果不存在当前事务,就和 REQUIRED 一样新建一个事务。

三、部分事务的不同点
3.1 NESTED 和 REQUIRES_NEW 的区别

  REQUIRES_NEW 是新建一个事务,并且新开始的这个事务和当前事务无关,当前事务回滚,不会影响到 REQUIRES_NEW 事务。
  而 NESTED 是一个嵌套事务,是父事务的一个子事务。当前事务存在时,NESTED 会开启一个嵌套事务。
  在 NESTED 情况下,父事务回滚时,子事务也会回滚,而 REQUIRES_NEW 情况下,原有事务回滚,不会影响新开启的事务。

3.2 NESTED 和 REQUIRED 的区别

  REQUIRED 情况下,当前事务存在时,被调用方和调用方使用同一个事务,那么被调用方出现异常时,由于共用一个事务,所以无论是否 catch 异常,事务都会回滚。
  而 NESTED 情况下,被调用方发生异常时,调用方可以 catch 其异常,这样只有子事务回滚,父事务不会回滚。

四、测试验证

  为了验证传播特性的准确性,笔者采用 Springboot,分别编写了上述伪代码中的两个测试方法,即调用方 ServiceA.saveA 方法和被调用方 ServiceB.saveB 方法,详见下方代码。

package com.test.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.test.entity.Stat;
import com.test.dao.sconl.StatMapper;

import javax.annotation.Resource;

@Service
public class ServiceA {
    
    

    @Resource
    public StatMapper statMapper;

    @Resource
    public ServiceB serviceB;

    @Transactional(value = "sconlTransactionManager", propagation = Propagation.REQUIRED)
    public void saveA() {
    
    
        Stat stat = new Stat();
        stat.setStatId("0001");
        stat.setStatNm("0001");
        int resultA = statMapper.insertSelective(stat);
        System.out.println(resultA);

        // 调用 B 服务的方法
        try {
    
    
            serviceB.saveB();
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }

        // throw new RuntimeException("ExceptionA");
    }
}

package com.test.service;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import com.test.entity.OrderInfo;
import com.test.dao.sconl.OrderMapper;

import javax.annotation.Resource;

@Service
public class ServiceB {
    
    

    @Resource
    public OrderMapper orderMapper;

    @Transactional(value = "sconlTransactionManager", propagation = Propagation.NESTED)
    public void saveB() {
    
    
        OrderInfo orderInfo = new OrderInfo();
        orderInfo.setOrderId("orderId");
        int resultB = orderMapper.insertSelective(orderInfo);
        System.out.println(resultB);

        // throw new RuntimeException("ExceptionB");
    }
}

  上述两个测试类均采用声明式事务,通过 @Transactional 标签来声明事务,标签中的 value 属性设置事务管理器,propagation 属性设置事务传播特性,方法内部通过 throw new RuntimeException(“ExceptionB”) 来抛出异常。默认情况下,RuntimeException 异常会导致事务回滚。通过改变当前事务和子事务的传播特性,以及设置 saveA 和 saveB 方法内部是否抛出异常,来测试当前事务和子事务之间的事务传播特性。
  经过测试,结果和理论相符。下表总结下不同的事务传播特性和异常状态对结果的影响。
ServiceA.saveA() 以 Propagation.REQUIRED 修饰,ServiceB.saveB() 以表格中三种属性修饰,测试结果和一些关键性日志如下表所示(异常状态一列中,均以 A 表示 ServiceA.saveA() 方法,以 B 表示 ServiceB.saveB() 方法)。

异常状态 REQUIRED REQUIRES_NEW NESTED
A正常
B正常
A提交
B提交
A提交
B提交
A提交
B提交
A正常
B异常
A回滚
B回滚
A提交
B回滚
A提交
B回滚
Rolling back transaction to savepoint
A异常
B正常
A回滚
B回滚
A回滚
B提交
A回滚
B回滚
Releasing transaction savepoint
A异常
B异常
A回滚
B回滚
A回滚
B回滚
A回滚
B回滚

  测试时,将 logback 的应用日志级别和 root 日志级别设置为 DEBUG,则可以看到具体的事务信息。
  将子事务设置为 REQUIRED 时,一些关键性日志如下所示:

Fetched SqlSession [org.apache.ibatis.session.defaults.DefaultSqlSession@d093edf] from current transaction

  将子事务设置为 REQUIRES_NEW 时,一些关键性日志如下所示:

Suspending current transaction, creating new transaction with name [com.test.service.ServiceB.saveB]
	 

Resuming suspended transaction after completion of inner transaction

  将子事务设置为 NESTED 时,一些关键性日志如下所示:

Creating nested transaction with name [com.test.service.ServiceB.saveB]
	 

Releasing transaction savepoint
五、测试时踩过的坑

  笔者在实际代码中测试时,由于踩了一些坑,导致事务传播效果和理论不符,现在简单记录下来。

5.1 多数据源情况下未指明事务管理器

  笔者在多数据源项目中做测试,开始时未在事务标签中通过 value 属性指明事务管理器,出现了和理论不符的测试效果。后来在父方法和子方法中均指明了实际的数据源事务管理器,测试效果才和理论相符。

5.2 方法修饰符设置成 private

  开始时部分方法的修饰符设置为 private,导致事务失效,效果和理论不符。为了能被 AOP 事务增强,方法修饰符须为 public。

5.3 父事务方法未捕获子事务方法的异常

  开始时,父方法内部调用子方法时,未通过 try - catch 捕获子方法的异常,导致效果和理论不符。后来在 ServiceA.saveA() 方法内部调用 ServiceB.saveB() 时,用 try - catch 捕获了子事务方法的异常,测试效果终和理论相符了。

文章参考:

猜你喜欢

转载自blog.csdn.net/piaoranyuji/article/details/124709197