最近在整理以前的工作笔记,又发现一个有意思的bug
,在一个service
调用另外一个类的方法时,出现了一个Spring
的事务问题,Transaction rolled back because it has been marked as rollback-only,时间过去有点儿久了,不记得是压测时出的问题还是线上运行时出的问题了。
2021-01-25 19:42:00.025 [pool-3-thread-1] ERROR o.s.s.support.TaskUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task. org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724) at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:504) at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:292) at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:96) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.adapter.MethodBeforeAdviceInterceptor.invoke(MethodBeforeAdviceInterceptor.java:52) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:168) at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:92) at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:179) at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:673) at com.sowin.xszz.service.impl.BsBjServiceImpl$$EnhancerBySpringCGLIB$$35a078c9.addBj(<generated>) at com.sowin.xszz.task.XsBjTask.addBj(XsBjTask.java:23) at sun.reflect.GeneratedMethodAccessor248.invoke(Unknown Source) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:65) at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:54) at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81) at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511) at java.util.concurrent.FutureTask.run(FutureTask.java:266) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:180) at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:293) at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149) at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624) at java.lang.Thread.run(Thread.java:748)
上面是完整的详细错误信息,Transaction rolled back because it has been marked as rollback-only这句话翻译过来大体上的意思就是:事务已经回滚,因为它被标记为仅回滚,咋一听还是挺拗口,但是如果大家对spring
事务有一定了解的话,就很容易看出来,这个问题就是一个典型的嵌套事务问题。
我为什么强调是这是一个典型案例呢?
因为在很多时候,有些人害怕方法出错导致整个调用链崩溃,所以在自己写的方法中都会下意识的加上try catch
语句,而spring
的事务是利用aop
机制来实现的,也就是说提交事务和回滚事务这些操作都是它帮我们做的,我们并没有显示处理,那它之所以能够正确知道是提交事务还是回滚事务,这个就得靠我们给它信号了,没错,你们已经猜到了,抛异常就是我们给它的信号。
这么解释大家应该清楚了,应该也能大概猜到这问题多少都跟try catch
有关,不着急,我们接着往下看,后面会用到这块儿知识点,我们现在回到spring
事务本身来。
在spring的事务体系中,事务是可以继承的,它利用一个传播属性propagation
来标记各种继承关系(详细的继承关系这里就不细说,如果大家想了解清楚,请移步Srping事务的七种传播特性),默认的是REQUIRED
,也就是说里层的方法共用外层方法的事务。
为了给大家看的更清楚,我给大家简单模拟两句出问题的代码,这样有助于理解,模拟场景为方法A调用方法B,双方都加事务,然后B方法出错抛异常。
// 外层 @Transactional(readOnly = true, rollbackFor = Exception.class) public String A() { try { // 调用B方法 String s = B(); // ... return s; } catch(Exception e) { e.printStacktrace(); } return null; } // 里层 @Transactional(readOnly = true, rollbackFor = Exception.class) public String B() { // throw Exception return "test"; }
如果上面的分析看明白了,那这里就好理解了,A()
调用B()
时,B()
方法出错抛异常了,但是A()
方法呢,自己把异常给捕获了,而且在吃掉异常之后,它并没有做进一步处理,也没有继续向上抛,那此时就会出现本文中所提到的错误Transaction rolled back because it has been marked as rollback-only。
那可能有人又要问了,那A()
吃掉异常跟B()
有什么关系呢?
诶,问到点子上来了,我们上面提到了事务的继承关系,那么代入到这个案例中,也就是说B()
其实最终使用的还是A()
的事务,那么在B()
本身出异常时,它自己是已经抛过了的,此时它的transaction
已经被设置为了rollback-only
了,而当A()
将异常吃掉之后,统一交由spring
来提交事务,这时候B()
就会报这个错误了,大家可以简单理解一下,在这种嵌套事务中外层一旦吃掉异常之后,就会导致里层方法的事务状态不对,从而就会导致该错误的产生。
1、在try catch中手动处理transaction事务状态;
// 外层 @Transactional(readOnly = true, rollbackFor = Exception.class) public String A() { try { // 调用B方法 String s = B(); // ... return s; } catch(Exception e) { // 划重点,看这里 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly(); e.printStacktrace(); } return null; }
2、给每一层开启新事务,各自为政(如果涉及到db操作则不建议)
// 里层 // 重点看注解中的propagation参数 @Transactional(readOnly = true, rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW) public String B() { // throw Exception return "test"; }
这个问题其实真的很典型,尤其是新手,一不小心就可能遇到,如果项目是MVC
架构,那我们尽量不要在service
层中搞太多的try catch
,除非是必须的一些场景(如:多线程、IO),不要害怕抛异常,现在很多框架都会有全局异常处理器这个东西,很多时候,我们完全可以利用异常和它的搭配减轻很多重复代码。
转自:不忘初心 https://www.jiweichengzhu.com/article/9e7c096df3484dc1bd78c2b95a702620
文章评论
这是后端开发常见的事务异常,务必掌握!