踩坑:由于Spring自定义AOP导致Transaction rolled back because it has been marked as rollback-only问题

发现问题

在线上监控日志时发现了一个报错: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);
        }

    }
}

输出:
在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值