前言
最近项目上有一个使用业务相对复杂的业务场景报错了。在绝大多数情况下,都是风平浪静,没有问题。其实内涵暗潮涌动,在有些反常情况下就会报错,这种偶然性的问题很有可能就会在暴露到生产上造成事端,那究竟是怎样回事呢?
问题描述
咱们用一个简略的例子模拟下,咱们也能够看看下面这段代码输出的成果是什么。
- 在类
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旭阳】交流学习