首先我说下写这篇文章的原因,虽然平时也频繁使用spring的事务,但是对事务在复杂情况下发生回滚的情况和spring不同的事务传播行为还是很模糊。
 
因为平时都只使用默认的传播行为,其他的很少用。但在面对特殊的需求场景时,有时确实需要使用其它的传播行为来实现。
 
同时我自己对不同传播行为和发生异常后库表回滚的结果特别好奇,毕竟如果用错了事务很可能就把数据搞坏了个p的了,后果很严重啊!

所以这篇文章主要来测试在不同的传播行为下,事务回滚的结果如何。毕竟好记性不如烂笔头嘛!

这里因为篇幅原因只测试三种传播行为(REQUIRED、REQUIRES_NEW和SUPPORTS),虽然篇幅看上去有点长,但大部分都是贴的代码和日志,并没有那么复杂。相信对事务回滚和spring传播行为这块比较模糊的朋友看完后会有所收获。

不废话了,开搞!!

目录

  • 一、准备环境
    • 数据库表
    • 代码结构
      • TestService:
      • OneService:
  • 二、不同事务传播行为执行结果
    • 1、REQUIRED(默认)
      • (1)、子service业务异常
      • (2)、子service业务异常(内部try...catch捕获)
      • (3)、主service业务异常
      • (4)、主service业务异常(内部try...catch捕获)
    • 2、REQUIRES_NEW
      • (1)、子service业务异常
      • (2)、子service业务异常(内部try...catch捕获)
      • (3)、子service业务异常(主service中try...catch捕获)
      • (4)、主service业务异常
      • (5)、主service业务异常(内部try...catch捕获)
    • 3、SUPPORTS
  • 三、结论
  • 四、异常解释(Transaction rolled back because it has been marked as rollback-only)
    • 解决方式:

一、准备环境

数据库表


我这里准备了三张表,结构都一样,都只有idname字段。

代码结构


我们通过调用TestService里的insert()方法来完成所有的测试。这里将TestService作为主service,其余三个(OneServiceTwoServiceThreeService)作为子service,被TestService调用。
这三个子service中分别定义了上边三个表的insert操作。具体代码结构如下:

TestService:

@Service
@Slf4j
public class TestService {@AutowiredOneService oneService;@AutowiredTwoService twoService;@AutowiredThreeService threeService;@Transactionalpublic void insert(){TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);//插入one表TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);//插入two表TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);//插入three表log.info("over!");}
}

OneService:

@Service
@Slf4j
public class OneService {@AutowiredITableOneService tableOneService;public void insertOne(TestTableOne record){tableOneService.insertSelective(record);log.info("oneService插入");}
}

TwoServiceThreeServiceOneService内容基本一致,只有操作库表不同,所以不再贴代码了-_-

二、不同事务传播行为执行结果

spring的事务传播行为有以下几种:

传播行为 描述
REQUIRED(默认) Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。
REQUIRES_NEW 每次都会新建一个事务,如果上下文中有事务,则将上下文的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。
SUPPORTS 如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。
MANDATORY 上下文中必须要存在事务,否则就会抛出异常。
NOT_SUPPORTED 如果上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。
NEVER 上下文中不能存在事务,否则就会抛出异常。
NESTED 嵌套事务。如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。

测试前,我们要先清楚spring事务回滚的核心点

核心点:被@Transactional注解标注的方法会开启事务,当方法中出现异常时会回滚对库表的操作,而且这个异常是不能被捕获的,如果用try…catch捕获的话就不会回滚了,即事务失效,因为spring会认为业务处理没有出错也就不需要回滚。

下面开始测试不同的事务传播行为发生异常时库表数据的回滚情况。

1、REQUIRED(默认)

Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。

TestServiceinsert()方法使用REQUIRED级别时,主service子service同属于一个事务中,所以insert()insertOne()insertTwo()还有insertThree()中出现异常时会回滚所有的库表操作。下面分几种情况分别测试下。

首先我们先看下程序正常执行的日志情况:

库表结果:这里我把三个表的数据union在一起,方便查看。
)
从日志可以看到它们都处于同一个事务中,正常执行结束后事务会commiting提交。库表里入库三条数据。

那么当不同位置发生异常的时候事务会如何回滚呢?往下看。

(1)、子service业务异常

当子service的位置出现异常时,事务回滚是怎么的?

这里在insertTwo()中写个异常,看下日志结果,是否会回滚。

public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");int result = 1 / 0;//异常
}

日志:

库表:
)

这里因为insertTwo()方法报错,所以事务进行了回滚并没有提交,库表里没有插入的数据。

(2)、子service业务异常(内部try…catch捕获)

将上边第一种情况报异常的地方进行try…catch,看捕获异常后事务回滚的情况。

public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");try {int result = 1 / 0;//异常} catch (Exception e) {log.error("two service 出错了!");}
}

日志:

库表:

这里insertTwo()方法虽然报错但是在方法内就已经try…catch捕获,所以事务没有回滚,成功提交,库表里插入三条数据。

(3)、主service业务异常

insert()方法中出现异常。

     @Transactionalpublic void insert(){TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);//测试主service中报异常时放开int result = 1 / 0;//异常log.info("over!");}

这种情况结果同(1)相同,所以不再赘述。

(4)、主service业务异常(内部try…catch捕获)

    @Transactionalpublic void insert(){TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);//测试主service中报异常且需捕获时放开try {int result = 1 / 0;//异常} catch (Exception e) {log.info("master service 出错了!");}log.info("over!");}

这种情况结果同(2)相同,所以不再赘述。

2、REQUIRES_NEW

每次都会新建一个事务,如果上下文中有事务,则将上下文的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。

现在,我们只给insertTwo()方法加上@Transactional(propagation = Propagation.REQUIRES_NEW),表示在调用insertTwo()方法时会开启一个新的事务,继续测试不同的异常情况。

//TwoService中
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertTwo(TestTableTwo record){......
}

首先正常执行的日志如下:

可以看到一开始执行insert()方法时就开启了一个事务(红色框区域),在调用insertTwo()的时候又开启了新的事务(绿色框区域),并且先suspending挂起了之前的,等到insertTwo()执行结束并提交事务之后,第一个事务才resuming恢复继续执行并提交,符合上边对REQUIRES_NEW传播行为的描述。

那么发生异常情况的时候结果又会是什么样呢?比如上边我们测试REQUIRED时列举的几种异常场景。
相信各位经过上边的异常测试对这个问题已经有了答案。这不就是比刚才多了一个事务嘛,哪个事务发生异常肯定就回滚哪个呗,不同事务之间不影响,简单简单~~

虽说如此,但是还是有一点细节需要注意的,比如子service异常在自己方法内部捕获或者在外部主service捕获的区别,所以还是列举一下这几种结果,给大家吃个定心丸!

(1)、子service业务异常

测试代码如下:

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");int result = 1 / 0;//异常
}

日志:

库表:
)

可以看到insertTwo()发生异常后没有捕获,导致insertTwo()的事务没有提交,出现回滚,同时又因为insertTwo()方法是被主service调用,也就是相当于主service内部出现错误,所以也需要回滚。最终库表里没有数据。

(2)、子service业务异常(内部try…catch捕获)

将第一种情况报异常的地方进行try…catch。

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");try {int result = 1 / 0;//异常} catch (Exception e) {log.error("two service 出错了!");}
}

日志:

库表:
)

可以看到因为我们在insertTwo()方法中捕获了异常,所以insertTwo()主service的事务都成功提交,没有回滚。库表里插入三条数据。

那这里就有另一种情况了,如果我不在insertTwo()捕获异常而是在主service中捕获呢?接着往下看

(3)、子service业务异常(主service中try…catch捕获)

insertTwo()中不捕获异常,而在主service中捕获,代码如下:

 //TestService中@Transactionalpublic void insert(){try {TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);} catch (Exception e) {log.error("master service 捕获 子service 的异常!");}log.info("over!");}//TwoService中@Transactional(propagation = Propagation.REQUIRES_NEW)public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");int result = 1 / 0;//异常}

日志:

库表:

从日志可以看出,insertTwo()因为出现异常所以事务没有提交,而外部的主service因为捕获了异常所以能成功提交事务,也就是insertOne()数据插入成功,insertTwo()回滚,而insertThree()则是直接没执行到-_-

到这里大家应该也有结论了,不管是否是多个事务,只要这个事务里抛出了异常且不捕获,该事务就会回滚,否则就会正常提交。

(4)、主service业务异常

 @Transactionalpublic void insert(){TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);//测试主service中报异常时放开int result = 1 / 0;//异常log.info("over!");}//TwoService中@Transactional(propagation = Propagation.REQUIRES_NEW)public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");}

日志:

库表:
)

日志显示,主service业务报错,主事务回滚。insertTwo()的事务不受影响,正常提交。所以库表里只有test_table_two中有数据。

(5)、主service业务异常(内部try…catch捕获)

代码:

 @Transactionalpublic void insert(){TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);//测试主service中报异常且需捕获时放开try {int result = 1 / 0;//异常} catch (Exception e) {log.info("master service 出错了!");}log.info("over!");}//TwoService中@Transactional(propagation = Propagation.REQUIRES_NEW)public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");}

日志:

库表:

这种情况同正常情况结果相同,因为异常发生后立马被捕获,所以事务都没有回滚,正常提交库表中的三个表都有数据,所以不在赘述。

3、SUPPORTS

如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。

如上述所说,该传播行为有两种情况,第一种情况是加入现有事务,另一种是以非事务方式进行。所以我们先放上这两种情况正常执行的日志信息让大家看看差别。

情况一代码如下:

 //TestService中@Transactionalpublic void insert(){TestTableOne one = new TestTableOne();one.setName("name1");oneService.insertOne(one);TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);TestTableThree three = new TestTableThree();three.setName("name3");threeService.insertThree(three);log.info("over!");}//TwoService中@Transactional(propagation = Propagation.SUPPORTS)public void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");}

日志:

库表:
)

可以看到,因为主service有事务,所以在调用insertTwo()方法时,insertTwo()的操作加入了该事务。

情况二代码:

和情况一代码基本一致,只是insert()方法去掉了Transactional()注解,所以省略了重复部分,免得看着代码太多脑壳疼(°_°)。

 //TestService中  public void insert(){......}//TwoService中@Transactional(propagation = Propagation.SUPPORTS)public void  insertTwo(TestTableTwo record){......}

日志:

看出差别了吧各位。在主service没有事务的情况下(no transactional),insertTwo()方法就以非事务方式执行,没有了之前测试的时候事务提交的那一步:committing SqlSession

这就是SUPPORTS传播行为的特性,那么发生异常情况的时候回滚问题还是和之前一样,事务抛出异常就回滚,捕获了就不回滚。这里就不再贴代码测试了。

好了,其余的传播行为就不一一测试了(挨个测试贴代码太烦了-_-),其实不管是哪个传播行为的异常回滚情况,只要了解了事务回滚的原因,就很轻松的能推断出库表中到底有没有提交或回滚,那我们就总结下吧!

三、结论

通过上边的测试,我们了解到不同的传播行为在发生不同情况的异常时事务回滚的情况。
其实事务的传播行为只是让场景变得复杂了些而已,因为在实际开发中有时候确实需要使用不同的传播行为满足需求,但是事务是否会回滚其实很简单,还是那个核心点

当开启事务后,方法中如果出现异常且未被捕获,那么该事务就会进行回滚,但如果这个异常被捕获,那么spring就会认为我们的业务逻辑没有错误,也就不会让事务进行回滚。

核心思想很简单,复杂的只是所处的场景而已,经过一层一层分析,问题都会变的很简单^ ^

四、异常解释(Transaction rolled back because it has been marked as rollback-only)

org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

这里解释一下这个异常出现的原因和解决方式,因为这也是我写代码测试过程中遇到的,是因为对异常处理的方式不恰当引起的。

这个报错的根本原因是:在一个事务中,事务已经被标记为rollback-only回滚,但是后续又commit提交事务,属于自相矛盾,所以报了这个异常。虽然库表也会回滚,但是有报错还是不太好,需要解决下。

先模拟出这个报错,代码如下:

 //TestService中@Transactionalpublic void insert(){try {TestTableTwo two = new TestTableTwo();two.setName("name2");twoService.insertTwo(two);} catch (Exception e) {log.error("two service 出错了!");}}//TwoService中@Transactionalpublic void  insertTwo(TestTableTwo record){tableTwoService.insertSelective(record);log.info("twoService插入");int result = 1 / 0;//异常}

这个代码当调用insert()方法时会报上边的异常,为什么呢?

这里insert()insertTwo()都加了@Transactional注解,而且都是默认的传播行为。所以当insert()调用insertTwo()方法时,两个事务合二为一,是属于一个事务。

insertTwo()里出现异常时应该回滚,所以把这个事务标记成rollback-only,但是异常又被主service捕获,主service的操作没有报错,所以执行完后又想提交,自相矛盾,最后出现上述异常。

解决方式:

我们已经知道问题出现的原因,那么就把问题出现的条件破坏掉就好。
核心思想是:事务被标记为回滚后,就不能再提交。

这里可以在主service的catch里继续throw抛出异常,主service也就不会再去提交事务了。

可能有人会有疑问,这波操作有什么意义吗,你本来就进行try…catch了,还继续抛出干嘛,脑子进泡了吧!但其实仔细想想,上边这种情况本来就是因为错误的写法导致的问题吧,正常情况下如果发生异常,我们肯定是想要库表进行回滚的,如果平白无故把异常吃掉不进一步处理,库表数据肯定是有问题的。

所以这里只是给出了出现这个异常处理的方式。另外说下,其实这里try…catch也是可以的,因为我们可能想自定义异常信息抛出,就可以在catch里throw,具体看自己的需求想要什么样的效果吧。

好了,这就是所有的内容了,我下班了!如果有想交流的,请评论留言,看到就回!

【Spring】Spring不同事务传播行为测试相关推荐

  1. Spring七种事务传播行为

    事务传播行为 "事务传播行为"描述的是:当一个事务方法被另一个方法调用时,该事务方法如何进行? 是创建新事务?丢弃事务?还是加入到已存在的事务呢? 针对这些情况,Spring框架定 ...

  2. spring 七种事务传播行为

    spring事务传播行为详解 一.什么是事务传播行为? 二.Spring中七种事务传播行为 三.REQUIRED,REQUIRES_NEW,NESTED异同 四.三种Transactional不回滚问 ...

  3. Spring中的事务传播行为

    前言:在Spring中,我们可以通过声明式事务,实现对数据库操作的事务管理.其中,在声明式事务时,有一个事务的属性为propagation,即事务的传播行为.今天,就来讨论该属性的作用. 什么是事务的 ...

  4. spring注解@transactional事务传播

    文章目录 事务传播 传播类型 REQUIRED SUPPORTS MANDATORY REQUIRES_NEW NOT_SUPPORTED NEVER NESTED 事务传播 在spring中使用事务 ...

  5. spring Transaction Propagation 事务传播

    spring Transaction中有一个很重要的属性:Propagation.主要用来配置当前需要执行的方法,与当前是否有transaction之间的关系. 我晓得有点儿抽象,这也是为什么我想要写 ...

  6. spring 事务隔离级别和传播行为_Java工程师面试1000题146-Spring数据库事务传播属性和隔离级别...

    146.简介一下Spring支持的数据库事务传播属性和隔离级别 介绍Spring所支持的事务和传播属性之前,我们先了解一下SpringBean的作用域,与此题无关,仅做一下简单记录. 在Spring中 ...

  7. java spring 事务传播_spring事务传播机制实例讲解

    天温习spring的事务处理机制,总结如下 对于SQL事务的概念以及ACID性质,可以参见我的另一篇博文 http://kingj.iteye.com/admin/blogs/1675011 spri ...

  8. Spring 事务传播机制 实例讲解

    事务传播机制 对于SQL事务的概念以及ACID性质,可以参见我的另一篇博文 http://kingj.iteye.com/admin/blogs/1675011 spring的管理的事务可以分为如下2 ...

  9. spring事务(Transaction)的七种事务传播行为及五种隔离级别

    1. 首先,说说什么事务(Transaction) 事务,就是一组操作数据库的动作集合.事务是现代数据库理论中的核心概念之一. 如果一组处理步骤或者全部发生或者一步也不执行,我们称该组处理步骤为一个事 ...

最新文章

  1. QT 信号与槽 最简单例子
  2. getRunningTask API
  3. 基于事件的 NIO 多线程服务器--转载
  4. 信息抽取(一)机器阅读理解——样本数据处理与Baseline模型搭建训练(2020语言与智能技术竞赛)
  5. postgis启动_postgresql的安装和启动方法
  6. ImportError: cannot import name HTTPSHandler
  7. 这些人,建议你不要去贷款了
  8. socket用法linux,linux socket编程,要用到哪些函数,和用法介绍?_Linux_天涯问答_天涯社区...
  9. mysql+tushare搭建本地数据库
  10. youcans 的 OpenCV 学习课—8.频率域图像滤波(上)
  11. java https安全传输
  12. WDCP + CentOS 6.x + EduSoho + Nginx
  13. Java基础篇:简单介绍一下final
  14. 微信 openid 变吗?
  15. 大学生软件设计大赛文档要求
  16. 配置Microsoft Visual SourceSafe 2005的Internet访问
  17. Android设置来电铃声和分享操作
  18. 德累斯顿工业大学计算机学院,德累斯顿工业大学好不好
  19. 计算机英语句子及翻译,简单的常用英语句子带翻译
  20. 周易六十四卦——山泽损卦

热门文章

  1. 排序算法之Bogo排序
  2. 幼儿园调查过程怎么写_如何观察?如何分析和记录?幼儿园观察记录应该怎么写?...
  3. MySql其实很好入门
  4. 2022年全球市场成人尿裤生产线总体规模、主要生产商、主要地区、产品和应用细分研究报告
  5. WINDOWS无法搜索新更新错误代码80070422
  6. 从头造一个SSD网络(1):理论篇
  7. python回调函数与eval函数
  8. 前沿应用丨大规模无人机集群与“虚实结合”半实物仿真系统
  9. 荣之学:传统国际国代和跨境电商物流不同之处有哪些?
  10. 微博评论情感分析(NLP,LSTM)