前言
针对spring中的事务传播机制,这个是面试的高频问题,但是往往有时候缺乏实例支撑,很难记住相关内容,这篇博客在实例的基础上,总结一下spring的事务传播机制。
准备工作
通常spring的事务注解是用在方法上,如下所示
1、相关数据SQL
准备一个数据库,其中有一张数据表,SQL如下
DROP TABLE IF EXISTS `stu`;
CREATE TABLE `stu` (
`id` int NOT NULL AUTO_INCREMENT,
`name` varchar(255) NOT NULL,
`age` int NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1221 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
2、准备一个基于springboot的项目,并引入测试
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
3、业务代码准备
引入mybatis啥的,这就不介绍了,各位看官百度即可。
服务类的接口和实现
public interface StuService {
public void saveParent();
public void saveChildren();
}
@Service("stuService")
public class StuServiceImpl implements StuService {
public void saveParent() {
Stu stu = new Stu();
stu.setName("parent");
stu.setAge(19);
stuMapper.insert(stu);
}
public void saveChildren() {
saveChild1();
saveChild2();
}
public void saveChild1() {
Stu stu1 = new Stu();
stu1.setName("child-1");
stu1.setAge(11);
stuMapper.insert(stu1);
}
public void saveChild2() {
Stu stu2 = new Stu();
stu2.setName("child-2");
stu2.setAge(22);
stuMapper.insert(stu2);
}
}
用于测试事务的服务类和接口
public interface TestTransService {
public void testPropagationTrans();
}
@Service("testTransService")
public class TestTransServiceImpl implements TestTransService {
@Autowired
private StuService stuService;
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
}
测试类代码
@RunWith(SpringRunner.class)
@SpringBootTest(classes = FoodieApiApplication.class)
public class TransTest {
@Autowired
private TestTransService testTransService;
@Test
public void myTest(){
testTransService.testPropagationTrans();
}
}
至此准备工作完成,可以基于这个简单的实例,来总结一下spring中事务的传播机制是个啥。在目前没有事务的时候,我们运行实例,数据库中会正常存入如下数据 如果在saveChildren方法中人为构造一个异常
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
运行会抛异常,同时child-2数据无法存入
child-2无法存入
以上的简单实例,会贯穿整篇博客。为了数据显示清晰,建议每次运行实例前,清空数据表中的数据。
spring中的几种事务传播机制
所谓的事务的传播,简单的理解就是多个事务方法相互调用时,事务如何在这些方法间传播。
举个栗子,方法A是一个事务的方法,方法A执行过程中调用了方法B,那么方法B有无事务以及方法B对事务的要求不同都会对方法A的事务具体执行造成影响,同时方法A的事务对方法B的事务执行也有影响,这种影响具体是什么就由两个方法所定义的事务传播类型所决定。
spring中总共定义了七中事务传播行为,主要是7个枚举值,具体如下
REQUIRED
REQUIRED的传播行为是spring中默认的传播行为。按照源码中的注释,表示的是当前方法支持当前事务,如果当前不存在事务,则会创建一个事务。直接通过实例来说明吧。
1、父方法开启事务,子方法不开启事务,且存在异常
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
在运行抛出异常之后,会发现数据库中没有任何数据存入,这个是因为子方法中存在异常,而导致的父方法的事务回滚,如果使用try-catch将子方法异常处理掉,则会有相关数据保存成功。
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void testPropagationTrans() {
stuService.saveParent();
try {
stuService.saveChildren();
}catch (Exception e){
}
}
因此在实例中,我们重点需要关注的,是子方法的事务传播机制的枚举值
2、父方法无需事务,子方法开启事务,但存在异常
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.REQUIRED)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
这种情况下,父方法中的数据会保存成功,但是子方法会因为事务回滚未存储任何数据。
因为对于子方法来说,子方法的执行需要事务,父方法没有事务,子方法就自己创建了一个事务。
3、父方法开启事务,子方法也开启事务,且子方法存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.REQUIRED)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
这个时候,由于父方法存在事务,而子方法需要事务,子方法的传播机制配置是REQUIRED,这个时候子方法会和父方法共用一个事务(二者是同一个事务),由于子方法执行出现异常,导致整个事务回滚,因此没有任何数据存入成功。
SUPPORTS
关于SUPPORTS,源码中的注释是:支持当前事务,如果不存在则以非事务方式执行。从字面意思来理解,就是当前被Transaction注解修饰的方法,对事务是支持的态度,但是如果调用当前方法的父方法没有事务,则当前方法不会按照事务的模式来运行
1、父方法无事务,子方法的事务传播行为为SUPPORT,且存在异常
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.SUPPORTS)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
这个时候,父方法和子方法的运行结果,与没有事务的时候一样。
2、父方法开始事务,子方法的事务传播行为为SUPPORT,且存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.SUPPORTS)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
由于父方法存在事务,子方法在被父方法调用的时候,存在异常,事务会回滚,这个时候没有任何数据存入成功。
在父方法中catch处理掉子方法的异常,依旧没有任何数据存入成功
@Transactional(propagation = Propagation.REQUIRED)
@Override
public void testPropagationTrans() {
stuService.saveParent();
try {
stuService.saveChildren();
}catch (Exception e){
return;
}
}
MANDATORY
MANDATORY的中文意思是强制的。从源码的注释中得知,标示当前方法是支持事务的,但是如果当前方法没有在事务环境下运行,则会抛出异常。
1、父方法无事务,子方法的事务传播属性配置为MANDATORY,子方法无异常
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.MANDATORY)
public void saveChildren() {
saveChild1();
saveChild2();
}
这个时候运行的时候会报错
异常提示也很清楚,当前方法事务传播行为配置为MANDATORY,但程序没有提供任何事务的运行环境,故而子方法抛错。但是这个时候,父方法是没有事务的额,故而数据库中会保存成功一条数据
2、父方法开启事务,子方法的事务传播属性配置为MANDATORY,子方法无异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.MANDATORY)
public void saveChildren() {
saveChild1();
saveChild2();
}
所有数据正常存入
2、父方法开启事务,子方法的事务传播属性配置为MANDATORY,但子方法存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.MANDATORY)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
子方法的异常,导致事务回滚,无任何数据存入。即使父方法中catch处理了异常,依旧无法存入任何数据。
REQUIRES_NEW
从源码中的注释,可以得知,REQUIRES_NEW表示当前方法的逻辑会在一个新的事务中运行,但如果调用这个方法的父方法存在事务则当前方法会挂起父方法的事务。
1、父方法无事务,子方法配置为REQUIRES_NEW,且子方法存在异常
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
这个时候,**子方法会单独开启一个事务,但是由于本身存在异常,会导致回滚因此子方法不会保存任何数据,但是父方法由于没有事务,因此会保存一条数据。**从这一点可以看出,子方法的事务和父方法的事务是完全的两个事务
2、父方法开启事务(REQUIRED),子方法配置为REQUIRES_NEW,且子方法存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
正常来讲,由于子方法的事务和父方法的事务是完全不同的两个事务,子方法的回滚应该不影响父方法,但是由于子方法是抛出异常,会导致父方法也会出现异常,因此这个实例中,不会有任何数据存入。如果在父方法中catch处理子方法抛出的异常,则会存入一条parent的数据,这个就不贴图说明了,跑下实例即可。
3、父方法开启事务,但存在异常,子方法配置为REQUIRES_NEW,且子方法无异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
int a = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void saveChildren() {
saveChild1();
saveChild2();
}
运行结果:子方法的数据存入成功,父方法的数据存入失败。这一点更加说明了子方法的事务和父方法的事务并不是同一个。子方法执行的时候挂起了父方法的事务,子方法执行完成之后,在执行父方法,父方法因为异常回滚。
为了更好的理解REQUIRES_NEW和REQUIRED的区别,这里在加一个简单的实例
4、父方法开启事务(REQUIRED),但存在异常,子方法配置为REQUIRED,且子方法无异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
int a = 1 / 0;
}
@Transactional(propagation = Propagation.REQUIRED)
public void saveChildren() {
saveChild1();
saveChild2();
}
这个时候,子方法和父方法,没有任何一个数据保存成功,因为REQUIRED的传播机制下,子方法和父方法用的是同一个事务。
NOT_SUPPORTED
从字面意思可以理解,标示当前方法的运行不支持事务,从源码的注解中可以得知,标示目标方法以非事务的方式执行,如果当前存在事务则会挂起当前事务。
1、父方法无事务,子方法配置为NOT_SUPPORTED,且存在异常
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.REQUIRED)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
运行结果和父方法与子方法都没有事务的时候一样
2、父方法存在事务,子方法配置为NOT_SUPPORTED,且存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.NOT_SUPPORTED)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
运行结果
父方法会因为子方法的异常而回滚无法存入数据,而子方法的运行本身不支持事务,在方法在运行完成之后,会存入一条数据,子方法在运行的时候会挂起父方法的事务。
如果在上面实例的基础上,通过catch处理子方法的异常,则父方法的数据又能正常存入了。
NEVER
当前方法无论如何都无法在事务中运行,如果存在当前事务,则会抛出异常。
1、父方法存在事务,子方法配置为NEVER,且存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.NEVER)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
运行结果:无任何数据存入,这个时候运行会抛出异常,异常信息如下。
由于抛出了异常,子方法由于在调用之前就抛出了异常,因此第一笔数据都无法存入。父方法没有进行异常处理,故而也没有数据存入。当然,如果父方法catch了子方法的异常,则会顺利存入parent的数据。
2、父方法关闭事务,子方法配置为NEVER,且存在异常
@Override
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.NEVER)
public void saveChildren() {
saveChild1();
int a = 1 / 0;
saveChild2();
}
这个时候和没有事务的时候运行结果一样,这里不再贴出了
NESTED
NESTED的中文意思为内嵌的,嵌套。从源码的注释中,可以看到,表示如果当前方法的运行存在事务,则当前方法会以一个内嵌的事务嵌入到当前事务中,如果当前的运行环境没有事务,则等同于REQUIRED。
1、父方法存在事务,且存在异常,子方法配置为NESTED,不存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
int a = 1/0;
}
@Transactional(propagation = Propagation.NESTED)
public void saveChildren() {
saveChild1();
saveChild2();
}
运行的结果是没有任何数据存入,因为子方法是以内嵌的事务嵌入到父方法的事务中,父方法的事务回滚,子方法的事务也得回滚。
2、父方法存在事务,无异常,子方法配置为NESTED,存在异常
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
stuService.saveChildren();
}
@Transactional(propagation = Propagation.NESTED)
public void saveChildren() {
saveChild1();
int a = 1/0;
saveChild2();
}
依旧存储失败,因为子方法内嵌的事务出现异常回滚,导致父方法的事务也回滚,但是这并不表示子方法和父方法的事务是同一个,这里如果在父方法中catch处理掉子方法的异常,父方法的数据依旧会存入成功,主方法的事务并不会受到影响,这个也是NETESTED区别于REQUIRED的一点。
@Override
@Transactional(propagation = Propagation.REQUIRED)
public void testPropagationTrans() {
stuService.saveParent();
try {
stuService.saveChildren();
}catch (Exception e){
e.printStackTrace();
return;
}
}
@Transactional(propagation = Propagation.NESTED)
public void saveChildren() {
saveChild1();
int a = 1/0;
saveChild2();
}
父方法中通过catch处理掉子方法的异常,依旧能顺利存入数据
小结
在很多实例的基础上,梳理了一下spring中事务传播机制的问题,这里梳理一下
传播机制 | 含义 | 异常回滚情况 |
---|
REQUIRED | 当前方法的运行需要事务。 如果调用当前方法的父方法存在事务,则无需新建事务。 如果调用当前方法的父方法不存在事务,则当前方法会独立创建一个新的事务。 | 如果子方法因异常回滚,则父方法的事务也会回滚。 如果父方法存在事务,则子方法和父方法共用同一个事务,这个时候即使在父方法中catch处理子方法的异常,父方法的事务依旧会回滚 | SUPPORTS | 当前方法的运行仅仅对事务持支持态度,如果调用该方法的父方法存在事务,则当前方法就在事务环境中运行,如果父方法中不存在事务,则当前方法也不会按照事务的方式运行 | 如果父方法存在事务,则子方法和父方法共用同一个事务,这个时候即使在父方法中catch处理子方法的异常,父方法的事务依旧会回滚 | MANDATORY | 当前方法的运行强制需要在事务的环境下运行,如果没有则直接抛出异常 | 如果父方法存在事务,则子方法和父方法共用同一个事务,这个时候即使在父方法中catch处理子方法的异常,父方法的事务依旧会回滚 | REQUIRES_NEW | 当前方法的逻辑会在一个新的事务中运行,但如果调用这个方法的父方法存在事务则当前方法会挂起父方法的事务。 | 由于子方法和父方法用的不是同一个事务,因此如果父方法catch处理了子方法的异常,父方法的事务不会回滚 | NOT_SUPPORTED | 表示当前方法以非事务的方式执行,如果当前存在事务则会挂起当前事务 | 如果父方法中catch处理了子方法的异常,则子方法的异常并不会影响父方法事务的提交 | NEVER | 当前方法无论如何都无法在事务中运行,如果存在当前事务,则会抛出异常 | 父方法中catch处理了子方法的异常,则子方法的异常并不会影响父方法事务的提交 | NESTED | 如果当前方法的运行存在事务,则当前方法会以一个内嵌的事务嵌入到当前事务中,如果当前的运行环境没有事务,则等同于REQUIRED。 | 父方法事务的回滚会影响到子方法事务的回滚 |
|