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

所以这篇文章就是来测试在不同的传播行为下,事务回滚的结果如何。毕竟好记性不如烂笔头嘛!
这里因为篇幅原因只测试三种传播行为(REQUIRED、REQUIRES_NEW和SUPPORTS),虽然篇幅看上去有点长,但大部分都是贴的代码和日志结果,并没有那么复杂。
耐心看完,相信对spring事务传播行为和事务回滚这块儿比较模糊的朋友看完后会有所收获。
不废话了,开搞!!
目录
一、准备环境
数据库表

我这里准备了三张表,结构都一样,只有id和name字段。
代码结构

我们通过调用TestService里的insert()方法来完成所有的测试。
这里将TestService作为主service,其余三个(OneService、TwoService和ThreeService)作为子service,被TestService调用。
这三个子service里分别定义了上边三个表的insert操作。具体代码如下:
TestService:
@Service
@Slf4j
public class TestService {
@Autowired
OneService oneService;
@Autowired
TwoService twoService;
@Autowired
ThreeService threeService;
@Transactional
public 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 {
@Autowired
ITableOneService tableOneService;
public void insertOne(TestTableOne record){
tableOneService.insertSelective(record);
log.info("oneService插入");
}
}
子service的内容基本一致,其他两个就不多余贴了-_-
二、不同事务传播行为执行结果
spring的事务传播行为有以下几种:
| 传播行为 | 描述 |
|---|---|
| REQUIRED(默认) | Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。 |
| REQUIRES_NEW | 每次都会新建一个事务,如果上下文中有事务,则将上下文的事务挂起,当新建事务执行完成以后,上下文事务再恢复执行。 |
| SUPPORTS | 如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。 |
| MANDATORY | 上下文中必须要存在事务,否则就会抛出异常。 |
| NOT_SUPPORTED | 如果上下文中存在事务,则挂起事务,执行当前逻辑,结束后恢复上下文的事务。 |
| NEVER | 上下文中不能存在事务,否则就会抛出异常。 |
| NESTED | 嵌套事务。如果上下文中存在事务,则嵌套事务执行,如果不存在事务,则新建事务。 |
在测试前,我们要先清楚spring事务回滚的核心点:。
核心点:被
@Transactional注解标注的方法会开启事务,当方法中出现异常时会回滚对库表的操作,而且这个异常是不能被捕获的,如果用try…catch捕获的话就不会回滚,即事务失效,因为spring会认为业务处理没有出错也就不需要回滚。
下面开始测试不同的事务传播行为发生异常时数据的回滚情况。
1、REQUIRED(默认)
Spring 默认的事务传播级别,如果上下文中已经存在事务,那么就加入到事务中执行,如果当前上下文中不存在事务,则新建事务执行。
当TestService的insert()方法使用REQUIRED级别时,主service和子service同属于一个事务,所以上面代码中insert()、insertOne()、insertTwo()还有insertThree()中出现异常时都会回滚所有的库表操作。下面分几种情况验证。
首先我们先看下程序正常执行的日志情况:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-jH5FRQws-1648747136002)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220330141022159.png)]](https://i-blog.csdnimg.cn/blog_migrate/1603b2facf44bad3ca9ad7ef5e856295.png)
库表结果:这里我把三个表的数据union在一起,方便查看。

从日志可以看到它们都处于同一个事务中,正常执行结束后事务会commiting提交。库表里有三条数据。
那么当不同位置发生异常的时候事务会如何回滚呢?往下看。
(1)、子service业务异常
当子service的位置出现异常时,事务回滚是怎样的呢?
我这里在insertTwo()中写个异常,看下日志结果,是否会回滚。
public void insertTwo(TestTableTwo record){
tableTwoService.insertSelective(record);
log.info("twoService插入");
int result = 1 / 0;//异常
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Pj4QhZo1-1648747136002)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220330153456711.png)]](https://i-blog.csdnimg.cn/blog_migrate/e99fd7e006764f8e861175ab9cd7b597.png)
库表结果:
)
这里因为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 出错了!");
}
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QRouaIpI-1648747136003)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220330160736842.png)]](https://i-blog.csdnimg.cn/blog_migrate/15be1e122d4c6599f97cd239ad8ddcf8.png)
库表结果:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dQpVBGPQ-1648747136003)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220331184420788.png)]](https://i-blog.csdnimg.cn/blog_migrate/cd0aa42bfbd2a121c24c2db0bdacd48c.png#pic_center)
这里insertTwo()方法虽然报错但是在方法内部就已经try…catch捕获,所以事务没有回滚,成功提交,库表里插入三条数据。
(3)、主service业务异常
当insert()方法中出现异常。
@Transactional
public 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)、子service业务异常相同,所以不再赘述。
(4)、主service业务异常(内部try…catch捕获)
@Transactional
public 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)、子service业务异常(内部try...catch捕获)相同,不再赘述。
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捕获的区别,所以还是列举一下这几种结果,给大家吃个定心丸!
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rO9AYv1e-1648747136004)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220401005454689.png)]](https://i-blog.csdnimg.cn/blog_migrate/5aa58f0e0ea1a05df58ad74c60f9d4ab.png#pic_center)
(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 出错了!");
}
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XOUd1aQc-1648747136004)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220330234333147.png)]](https://i-blog.csdnimg.cn/blog_migrate/502b34a27fa046b54cb5356c398973cb.png)
库表结果:
)
可以看到因为我们在insertTwo()方法中捕获了异常,所以insertTwo()和主service的事务都成功提交,没有回滚。库表里插入三条数据。
那这里就有另一种情况了,如果我不在insertTwo()捕获异常而是在主service中捕获呢?接着往下看
(3)、子service业务异常(主service中try…catch捕获)
insertTwo()中不捕获异常,而在主service中捕获,代码如下:
//TestService中
@Transactional
public 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;//异常
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4bjGrhBs-1648747136005)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220330235345242.png)]](https://i-blog.csdnimg.cn/blog_migrate/9cf8cc96c786ca8da6be1168b0b14415.png)
库表结果:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LdgjJavS-1648747136005)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220331233642388.png)]](https://i-blog.csdnimg.cn/blog_migrate/7e6943ddea5b1c25b2940ba4089fa85b.png#pic_center)
从日志可以看出,insertTwo()因为出现异常所以事务没有提交,而外部的主service因为捕获了异常所以能成功提交事务,也就是insertOne()数据插入成功,insertTwo()回滚,而insertThree()则是直接没执行到-_-
到这里大家应该也有结论了,不管是一个事务还是多个事务,只要这个事务里抛出了异常且不捕获,该事务就会回滚,否则就可以正常提交。
(4)、主service业务异常
@Transactional
public 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插入");
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZOXiXdk4-1648747136005)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220331142648556.png)]](https://i-blog.csdnimg.cn/blog_migrate/72f54dd5dca272c6dd58c021bc669b30.png)
库表结果:
)
日志显示,主service业务报错,主事务回滚。insertTwo()的事务不受影响,正常提交。所以库表里只有test_table_two中有数据。
(5)、主service业务异常(内部try…catch捕获)
代码:
@Transactional
public 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插入");
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sdjBB9Ci-1648747136005)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220401000418279.png)]](https://i-blog.csdnimg.cn/blog_migrate/9b3e7f2f9767c759e8aca76d2c66e395.png)
库表结果:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-hiwwZzXG-1648747136006)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220331184420788.png)]](https://i-blog.csdnimg.cn/blog_migrate/497f8fb21709071537679724f1e35c3f.png#pic_center)
这种情况同正常情况结果相同,因为异常发生后立马被捕获,所以事务都没有回滚,正常提交库表中的三个表都有数据,所以不再赘述。
3、SUPPORTS
如果上下文存在事务,则加入到事务执行,如果没有事务,则使用非事务的方式执行。
如上述所说,该传播行为有两种情况,第一种情况是加入现有事务,另一种是以非事务方式进行。所以我们先放上这两种情况正常执行的日志信息让大家看看差别。
情况一代码如下:
//TestService中
@Transactional
public 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插入");
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P30pegOr-1648747136006)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220331152830880.png)]](https://i-blog.csdnimg.cn/blog_migrate/c8a05aeb17fea2df0ab77234b906b973.png)
库表结果:
)
可以看到,因为主service有事务,所以在调用insertTwo()方法时,insertTwo()的操作加入了该事务。
情况二代码:
和情况一代码基本一致,只是insert()方法去掉了Transactional()注解,所以省略了重复部分,免得看着代码太多脑壳疼(°_°)。
//TestService中
public void insert(){
......
}
//TwoService中
@Transactional(propagation = Propagation.SUPPORTS)
public void insertTwo(TestTableTwo record){
......
}
日志:
![[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-USbv6xg7-1648747136006)(/Users/sifaheng/Library/Application Support/typora-user-images/image-20220331153936743.png)]](https://i-blog.csdnimg.cn/blog_migrate/cf937347ad761d2b86fd6f78623e0b31.png)
看出差别了吧各位。在主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中
@Transactional
public void insert(){
try {
TestTableTwo two = new TestTableTwo();
two.setName("name2");
twoService.insertTwo(two);
} catch (Exception e) {
log.error("two service 出错了!");
}
}
//TwoService中
@Transactional
public 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的REQUIRED、REQUIRES_NEW和SUPPORTS三种事务传播行为在异常发生时的数据回滚情况。测试结果显示,REQUIRED和SUPPORTS下异常未被捕获会导致整个事务回滚,而REQUIRES_NEW会根据异常是否发生在新事务内决定回滚。此外,还介绍了如何处理Transactionrolledbackbecauseithasbeenmarkedasrollback-only异常。

1086

被折叠的 条评论
为什么被折叠?



