一、什么是事务传播机制?
事务传播机制,顾名思义就是多个事务方法之间调用,事务如何在这些方法之间传播,是重新创建事务还是使用父方法的事务,父方法的回滚对子方法的事务是否有影响等等,这些都是事务传播机制决定的。
二、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 捕获了子事务方法的异常,测试效果终和理论相符了。