发现问题
在线上监控日志时发现了一个报错: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
at org.springframework.transaction.support.AbstractPlatformTransactionManager.processRollback(AbstractPlatformTransactionManager.java:870)
at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:707)
at org.springframework.transaction.interceptor.TransactionAspectSupport.completeTransactionAfterThrowing(TransactionAspectSupport.java:667)
at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:371)
at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:118)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.interceptor.ExposeInvocationInterceptor.invoke(ExposeInvocationInterceptor.java:95)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:691)
at com.leyo.service.biz.product.BasicProductService$$EnhancerBySpringCGLIB$$a517c40c.addMultiErpCode()
at com.leyo.web.controller.ErpProductSyncController.callback(ErpProductSyncController.java:70)
at com.leyo.web.controller.ErpProductSyncController$$FastClassBySpringCGLIB$$1aa08109.invoke()
at org.springframework.cglib.proxy.MethodProxy.invoke(MethodProxy.java:218)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.invokeJoinpoint(CglibAopProxy.java:771)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:163)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.validation.beanvalidation.MethodValidationInterceptor.invoke(MethodValidationInterceptor.java:119)
at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:186)
at org.springframework.aop.framework.CglibAopProxy$CglibMethodInvocation.proceed(CglibAopProxy.java:749)
at org.springframework.aop.aspectj.MethodInvocationProceedingJoinPoint.proceed(MethodInvocationProceedingJoinPoint.java:88)
at com.leyo.web.interceptor.ControllerMethodInterceptor.interceptor(ControllerMethodInterceptor.java:100)
at sun.reflect.GeneratedMethodAccessor343.invoke(Unknown Source)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
同一时间,我的工作邮箱也收到了另一个错误:
java.sql.BatchUpdateException:Duplication entry XXX
分析原因
从报错Transaction rolled back because it has been marked as rollback-only来看:
当前的事务在执行完成时,如果发现事务已经被标记为了回滚,所以无法提交成功。这种场景是基于两个前提,
(1)使用了默认的事务传播方式,Propagation.REQUIRED,如果当前没有事务,就新建一个事务,如果已经存在一个事务中,加入到这个事务中。
(2)存在嵌套事务,外层方法和内层方法都是用@Transactional注解,因为这样才会有产生事务代理。
所以肯定在回写ERP编码接口的方法中,有某个嵌套事务因为报错且被捕获了,导致外层事务依然正常执行。
通读业务代码,只发现Mybatisplus的saveOrUpdateBatch 是具有@Transactional注解的,属于嵌套事务,但是他并没有进行trycatch处理,因此理应是抛出异常并进行回滚的。而且邮件的报错跟线上监控日志不一样,我沿着这个思路,去看邮件是怎么实现的。
邮件切面实现代码如下:
@Around("@within(sendEmailForException) || @annotation(sendEmailForException)")
public Object around(ProceedingJoinPoint joinPoint, SendEmailForException sendEmailForException) throws Throwable {
//...若干代码
try {
result = joinPoint.proceed();
} catch (Throwable e) {
//这里重新抛出了一个Throwable异常
throw new Throwable(e);
}
return result;
}
根据源码,可以发现,它这里是重新抛出了一个Throwable异常,这时候我又产生了疑问,外层方法自定义的AOP和外层方法的事务处理哪个先执行。我进行了调试之后,发现是AOP先执行,然后再到事务切面处理。
而且,根据源码调查,在事务抛出异常后,会执行TransactionAspectSupport#completeTransactionAfterThrowing进行处理,其中有一步判断事务是否需要回滚:
if (txInfo.transactionAttribute != null
&& txInfo.transactionAttribute.rollbackOn(ex)) {
//回滚
}else{
//提交
}
txInfo.transactionAttribute.rollbackOn 内部的执行代码如下:
@Override
public boolean rollbackOn(Throwable ex) {
if (logger.isTraceEnabled()) {
logger.trace("Applying rules to determine whether transaction should rollback on " + ex);
}
RollbackRuleAttribute winner = null;
int deepest = Integer.MAX_VALUE;
//这里的rollbackRules就是注解指定的异常
if (this.rollbackRules != null) {
for (RollbackRuleAttribute rule : this.rollbackRules) {
//这里rule.getDepth 应该是在找ex及其父类中是否有包含rule代表的异常
int depth = rule.getDepth(ex);
if (depth >= 0 && depth < deepest) {
deepest = depth;
winner = rule;
}
}
}
if (logger.isTraceEnabled()) {
logger.trace("Winning rollback rule is: " + winner);
}
// User superclass behavior (rollback on unchecked) if no rule matches.
if (winner == null) {
logger.trace("No relevant rollback rule found: applying default rules");
return super.rollbackOn(ex);
}
return !(winner instanceof NoRollbackRuleAttribute);
}
这里的判断条件是,
如果指定了rollbackFor属性(指定回滚的异常),例如java.lang.Exception,那么判断抛出的当前异常及其父类异常是否包含Exception.class,如果找不到,就调用super.rollbackOn(ex),也就是判断抛出的异常是RuntimeException或者是Error。
txInfo.transactionAttribute.rollbackOn(ex)条件为true就会进入到回滚代码块,否则进入提交代码块。
由于这里通过EmailAspect重新抛出的异常是Throwable,Exception是Throwable的子类,因此没办法进入到回滚代码块当中,因此依然进入了提交代码块导致了这个报错。
解决方法
(1)将外层方法的@Transactional的rollbackFor属性指定为Throwable.class【避免报错rollback-only,使程序能够正常进入回滚流程】,或者将发邮件的注解的切面类的报错方式使用RuntimeException代替
(2)解决业务异常
附:重现marked as rollback-only问题的例子
表结构
-- 用户表
CREATE TABLE `user` (
`id` int(30) NOT NULL AUTO_INCREMENT,
`name` varchar(255) DEFAULT NULL,
`age` int(30) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=14 DEFAULT CHARSET=utf8mb4;
-- 书本表
CREATE TABLE `book` (
`id` int(11) NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(100) DEFAULT NULL COMMENT '书本名称',
`weight` int(11) DEFAULT NULL COMMENT '书本重要性',
`classify_name` varchar(100) DEFAULT NULL COMMENT '书本分类',
PRIMARY KEY (`id`),
UNIQUE KEY `book_un` (`name`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8mb4;
构造重现程序
基于MybatisPlus+springboot框架,关键代码如下:
service方法:
UserService#testRollbackOnlyError
@SendEmailForException
@Transactional(rollbackFor = Exception.class)
public void testRollbackOnlyError() {
Book book = new Book();
book.setName("A");
book.setWeight(1);
book.setClassifyName("AA");
bookService.saveOrUpdateBatch(Lists.newArrayList(book));
User user = new User();
user.setName("沉到底");
user.setAge(6777);
save(user);
}
@SendEmailForException注解定义
/**
* @author XBird
* @date 2022/5/3
**/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface SendEmailForException {
}
@SendEmailForException注解实现切面(这个注解主要是报错后发送邮件用的,这里为了讲解问题只是简单实现)
/**
* @author XBird
* @date 2022/5/3
**/
@Aspect
@Component
public class EmailAspect {
@Around(value = "@annotation(sendEmailForException)")
public void process(ProceedingJoinPoint joinPoint, SendEmailForException sendEmailForException) throws Throwable {
try {
Object result = joinPoint.proceed();
} catch (Throwable e) {
//这里把原先的异常重新抛出为Throwable
System.out.println("被捕获了异常");
throw new Throwable(e);
}
}
}
输出: