Spring中事务嵌套这么用一定得注意了!!
Posted JAVA旭阳
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring中事务嵌套这么用一定得注意了!!相关的知识,希望对你有一定的参考价值。
前言
最近项目上有一个使用事务相对复杂的业务场景报错了。在绝大多数情况下,都是风平浪静,没有问题。其实内在暗流涌动,在有些异常情况下就会报错,这种偶然性的问题很有可能就会在暴露到生产上造成事故,那究竟是怎么回事呢?
问题描述
我们用一个简单的例子模拟下,大家也可以看看下面这段代码输出的结果是什么。
- 在类
SecondTransactionService
定义一个简单接口transaction2
,插入一个用户,同时必然会抛出错误
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction2()
System.out.println("do transaction2.....");
User user = new User("tx2", "111", 18);
// 插入一个用户
userService.insertUser(user);
// 跑错了
throw new RuntimeException();
- 在另外一个类
FirstTransactionService
定义一个接口transaction1
,它调用transaction2
方法,同时做了try catch
处理
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction1()
System.out.println("do transaction1 .......");
try
// 调用另外一个事务,try catch住
secondTransactionService.transaction2();
catch (Exception e)
e.printStackTrace();
// 插入当前用户tx1
User user = new User("tx1", "111", 18);
userService.insertUser(user);
- 定义一个
controller
,调用transaction1
方法
@GetMapping("/testNestedTx")
public String testNestedTx()
firstTransactionService.transaction1();
return "success";
大家觉得调用这个http
接口,最终数据库插入的是几条数据呢?
问题结果
正确答案是数据库插入了0条数据。
同时控制台也报错了,报错原因是:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only
是否和你预想的一样呢?你知道是为什么吗?
原因追溯
其实原因很简单,我们都知道,一个事务要么全成功提交事务,要么失败全部回滚。如果出现在一个事务中部分SQL要回滚,部分SQL要提交,这不就主打的一个”前后矛盾,精神分裂“吗?
controller.testNestedTx()
||
/
FirstTransactionService.transaction1() REQUIRED隔离级别
||
||
|| 捕获异常,提交事务,出错啦
/ ||
FirstTransactionService.transaction2() REQUIRED隔离级别
|| ||
|| 抛出异常,标记事务为rollback only
=======================
- 事务的隔离级别为
REQUIRED
,那么发现没有事务开启一个事务操作,有的话,就合并到这个事务中,所以transaction1()
、transaction2()
是在同一个事务中。 transaction2()
抛出异常,那么事务会被标记为rollback only
, 源码如下所示:
transaction1()
由于try catch
异常,正常运行,想必就要可以提交事务了,在提交事务的时候,会检查rollback
标记,如果是true, 这时候就会抛出上面的异常了。源码如下图所示:
这下,是不是很清楚知道报错的原因了,那想想该怎么处理呢?
解决之道
知道了根本原因之后,是不是解决的方案就很明朗了,我们可以通过调整事务的传播方式分拆多个事务管理,或者让一个事务"前后一致",做一个诚信的好事务。
- 将
try catch
放到内层事务中,也就是transaction2()
方法中,这样内层事务会跟着外部事务进行提交或者回滚。
@Override
@Transactional(rollbackFor = Exception.class)
public void transaction2()
try
System.out.println("do transaction2.....");
User user = new User("tx2", "111", 18);
userService.insertUser2(user);
throw new RuntimeException();
catch (Exception e)
e.printStackTrace();
- 如果希望内层事务抛出异常时中断程序执行,直接在外层事务的
catch
代码块中抛出e
,这样同一个事务就都会回滚。 - 如果希望内层事务回滚,但不影响外层事务提交,需要将内层事务的传播方式指定为
PROPAGATION_NESTED
。PROPAGATION_NESTED
基于数据库savepoint
实现的嵌套事务,外层事务的提交和回滚能够控制嵌内层事务,而内层事务报错时,可以返回原始savepoint
,外层事务可以继续提交。
事务的传播机制
前面提到了事务的传播机制,我们再看都有哪几种。
PROPAGATION_REQUIRED
:加入到当前事务中,如果当前没有事务,就新建一个事务。这是最常见的选择,也是Spring中默认采用的方式。PROPAGATION_SUPPORTS
:支持当前事务,如果当前没有事务,就以非事务方式执行。PROPAGATION_MANDATORY
:支持当前事务,如果当前没有事务,就抛出异常。PROPAGATION_REQUIRES_NEW
:新建一个事务,如果当前存在事务,把当前事务挂起。PROPAGATION_NOT_SUPPORTED
:以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。PROPAGATION_NEVER
: 以非事务方式执行,如果当前存在事务,则抛出异常。PROPAGATION_NESTED
:如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED
类似的操作。
如何理解PROPAGATION_NESTED
的传播机制呢,和PROPAGATION_REQUIRES_NEW
又有什么区别呢?我们用一个例子说明白。
- 定义
serviceA.methodA()
以PROPAGATION_REQUIRED
修饰; - 定义
serviceB.methodB()以
表格中三种方式修饰; methodA
中调用methodB
;
总结
在我的项目中之所以会报“rollback-only
”异常的根本原因是代码风格不一致的原因。外层事务对错误的处理方式是返回true或false来告诉上游执行结果,而内层事务是通过抛出异常来告诉上游(这里指外层事务)执行结果,这种差异就导致了“rollback-only
”异常。大家也可以去review自己项目中的代码,是不是也偷偷犯下同样的错误了。
欢迎关注个人公众号【JAVA旭阳】交流学习
本文来自博客园,作者:JAVA旭阳,转载请注明原文链接:https://www.cnblogs.com/alvinscript/p/17342419.html
深入理解事务--Spring事务的传播机制
https://blog.csdn.net/yuanlaishini2010/article/details/45792069
事务的嵌套概念
如何改变默认规则:
1 让checked例外也回滚:在整个方法前加上 @Transactional(rollbackFor=Exception.class)
2 让unchecked例外不回滚: @Transactional(notRollbackFor=RunTimeException.class)
3 不需要事务管理的(只查询的)方法:@Transactional(propagation=Propagation.NOT_SUPPORTED)
spring事务传播属性
PROPAGATION_SUPPORTS -- 支持当前事务,如果当前没有事务,就以非事务方式执行。
PROPAGATION_MANDATORY -- 支持当前事务,如果当前没有事务,就抛出异常。
PROPAGATION_REQUIRES_NEW -- 新建事务,如果当前存在事务,把当前事务挂起。
PROPAGATION_NOT_SUPPORTED -- 以非事务方式执行操作,如果当前存在事务,就把当前事务挂起。
PROPAGATION_NEVER -- 以非事务方式执行,如果当前存在事务,则抛出异常。
PROPAGATION_NESTED -- 如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作。
前六个策略类似于EJB CMT,第七个(PROPAGATION_NESTED)是Spring所提供的一个特殊变量。
它要求事务管理器或者使用JDBC 3.0 Savepoint API提供嵌套事务行为(如Spring的DataSourceTransactionManager)
-
ServiceA {
-
-
void methodA() {
-
ServiceB.methodB();
-
}
-
-
}
-
-
ServiceB {
-
-
void methodB() {
-
}
-
-
}
PROPAGATION_REQUIRED
假如当前正要执行的事务不在另外一个事务里,那么就起一个新的事务比如说,ServiceB.methodB的事务级别定义为PROPAGATION_REQUIRED, 那么由于执行ServiceA.methodA的时候
PROPAGATION_SUPPORTS
如果当前在事务中,即以事务的形式运行,如果当前不再一个事务中,那么就以非事务的形式运行PROPAGATION_MANDATORY
必须在一个事务中运行。也就是说,他只能被一个父事务调用。否则,他就要抛出异常PROPAGATION_REQUIRES_NEW
PROPAGATION_NOT_SUPPORTED
当前不支持事务。比如ServiceA.methodA的事务级别是PROPAGATION_REQUIRED ,而ServiceB.methodB的事务级别是PROPAGATION_NOT_SUPPORTED ,那么当执行到ServiceB.methodB时,ServiceA.methodA的事务挂起,而他以非事务的状态运行完,再继续ServiceA.methodA的事务。PROPAGATION_NEVER
不能在事务中运行。假设ServiceA.methodA的事务级别是PROPAGATION_REQUIRED, 而ServiceB.methodB的事务级别是PROPAGATION_NEVER ,那么ServiceB.methodB就要抛出异常了。PROPAGATION_NESTED
在 spring 中使用 PROPAGATION_NESTED的前提:
1. 我们要设置 transactionManager 的 nestedTransactionAllowed 属性为 true, 注意, 此属性默认为 false!!!
2. java.sql.Savepoint 必须存在, 即 jdk 版本要 1.4+
3. Connection.getMetaData().supportsSavepoints() 必须为 true, 即 jdbc drive 必须支持 JDBC 3.0
确保以上条件都满足后, 你就可以尝试使用 PROPAGATION_NESTED 了.
Log Service配置事务传播
-
-
<beans xmlns="http://www.springframework.org/schema/beans"
-
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
-
xmlns:tx="http://www.springframework.org/schema/tx"
-
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.0.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-2.0.xsd http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.0.xsd">
-
<!-- configure transaction -->
-
-
<tx:advice id="defaultTxAdvice" transaction-manager="transactionManager">
-
<tx:attributes>
-
<tx:method name="get*" read-only="true" />
-
<tx:method name="query*" read-only="true" />
-
<tx:method name="find*" read-only="true" />
-
<tx:method name="*" propagation="REQUIRED" rollback-for="java.lang.Exception" />
-
</tx:attributes>
-
</tx:advice>
-
-
<tx:advice id="logTxAdvice" transaction-manager="transactionManager">
-
<tx:attributes>
-
<tx:method name="get*" read-only="true" />
-
<tx:method name="query*" read-only="true" />
-
<tx:method name="find*" read-only="true" />
-
<tx:method name="*" propagation="REQUIRES_NEW"
-
rollback-for="java.lang.Exception" />
-
</tx:attributes>
-
</tx:advice>
-
-
<aop:config>
-
<aop:pointcut id="defaultOperation"
-
expression="@within(com.homent.util.DefaultTransaction)" />
-
<aop:pointcut id="logServiceOperation"
-
expression="execution(* com.homent.service.LogService.*(..))" />
-
-
<aop:advisor advice-ref="defaultTxAdvice" pointcut-ref="defaultOperation" />
-
<aop:advisor advice-ref="logTxAdvice" pointcut-ref="logServiceOperation" />
-
</aop:config>
-
</beans>
以上是关于Spring中事务嵌套这么用一定得注意了!!的主要内容,如果未能解决你的问题,请参考以下文章