Spring事务失效的几种场景

前言

在上一篇博文中,我们对@Transactional源码进行了解析,这一篇我们来结合源码,分析一下我们面试、开发中经常遇到的事务失效问题

前置知识点

  • 如果类或者方法上存在@Transactional注解,则Spring会对原始对象进行动态代理。如果是cglib动态代理,则普通方法会进入DynamicAdvisedInterceptor拦截器,如果是jdk动态代理,则普通方法会进入JdkDynamicAopProxy拦截器
  • 代理对象会查找可以作用于当前方法的advice(Interceptor的父接口),然后构成拦截器链,链式调用各个拦截器的invoke方法。@Transactional注解最终注入的拦截器为TransactionInterceptor,执行相关方法都会进入这个拦截器

有兴趣的小伙伴可以阅读我的上篇博文,可以帮助大家更好理解本篇文章

Spring之@Transactional源码解析icon-default.png?t=N7T8https://blog.csdn.net/qq_38257958/article/details/136300388?spm=1001.2014.3001.5501

失效场景

1.访问修饰符

demo演示
    @Transactional
    private void methodA() {
        saveData();
        updateData();
    }
源码解析

TransactionInterceptor#invoke

TransactionAspectSupport#invokeWithinTransactionAbstractFallbackTransactionAttributeSource#getTransactionAttribute

AbstractFallbackTransactionAttributeSource#computeTransactionAttribute

结论:@Transactional只能作用于public描述的普通方法

2.方法用final修饰

demo演示
    @Transactional
    public final void methodB() {
        saveData();
        updateData();
    }
原因

含有@Transactional注解标注的方法或者类,会被InfrastructureAdvisorAutoProxyCreator这个BeanPostProcessor动态代理,普通public修饰的方法会经过拦截器链,然后执行其invoke方法。TransactionInterceptor的invoke方法会开启事务,但是final修饰的方法,不可以重写,不会进行方法增强,即不会执行TransactionInterceptor的invoke方法来开启事务

流程图

3.方法的自调用 

demo演示
    public void methodC() {
        this.methodD();
    }

    @Transactional
    public void methodD() {
        saveData();
        updateData();
    }
这个this到底是什么?(以cglib动态代理为例)

存在两种情况有拦截器链和没有拦截器链

 没有拦截器链

 有拦截器链 

我们得出结论

  •  没有拦截器链执行methodProxy.invoke(target, args)
  •  有拦截器链执行method.invoke(target, args)

CGLIB动态代理的FastClass机制

  • *.invoke(target,args) : 执行父类的方法
  • *.invoke(proxy,args) : 产生死循环
  • proxyMethod.invokeSuper(target,args) : 父类转子类根据里氏替换原则,抛出类型转换异常
  • proxyMethod.invokeSuper(proxy,args) : 子类方法(方法增强)

综上所述:不管有没有拦截器都会执行原始对象方法,this指的是目标对象

解决方法
1.获取代理对象

DynamicAdvisedInterceptor的intercept方法

如果我们将this.advised.exposeProxy这个属性设为true,就可以通过AopContext.currentProxy()获取代理对象

操作步骤

  1. 在配置类上加上@EnableAspectJAutoProxy注解,将exposeProxy属性设为true
  2. 改造代码
    public void methodC() {
        TransactionService proxy = (TransactionService) AopContext.currentProxy();
        proxy.methodD();
    }

PS : @EnableAspectJAutoProxy注解和@EnableTransactionManagement注解有很深的渊源,有兴趣的读者,可以阅读我的关于Spring AOP相关博文

2.实现ApplicationContextAware接口
package com.test.aop.service;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class TransactionService implements ApplicationContextAware {
    // 注意:不需要加@Autowired注解,spring在相应阶段会执行setApplicationContext方法
    private ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    // 没有@Transactional注解
    public void methodC() {
        TransactionService service = applicationContext.getBean(TransactionService.class);
        service.methodD();
    }

    @Transactional
    public void methodD() {
        saveData();
        updateData();
    }
}
3.新建一个service,将methodD挪到新service里面

4.不匹配的传播行为

之前我根据源码整理的流程图,不同传播行为的搭配可能会抛出异常

5.异常问题

相关源码TransactionAspectSupport#invokeWithinTransaction

我们重点关注completeTransactionAfterThrowing方法

我们再关注RuleBasedTransactionAttribute的rollbackOn方法

@Transactional注解的rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName属性,后期会被解析成RollbackRuleAttribute对象

case1:@Transactional注解未指定rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName

当我们未指定上述四个属性,会调用super.rollbackOn的方法

默认情况下,事务只有在遇到RuntimeException异常或者Error的时候才会回滚

case2:@Transactional注解指定rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName

当我们指定了四个属性中的一个或者多个,就会被解析成RollbackRuleAttribute(NoRollbackRuleAttribute),最后通过getDepth方法获取winner,如果winner是RollbackRuleAttribute旧回滚,否则就提交事务

getDepth方法主要判断抛出的异常与指定的异常之间的关系

  • 如果抛出异常和指定异常一致,则depth为0
  • 如果抛出异常是指定异常子类,则depth加1(递归判断)
  • 如果抛出异常为Throwable,则depth为-1

depth值越小(大于0),优先级越高

综上所述

  • 当我们自己catch住异常,不会回滚事务 原因 : 当我们未抛出异常,不会执行completeTransactionAfterThrowing方法,更不会执行回滚操作
  • 未指定rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName属性,但是抛出的不是RuntimeException(Error)异常 原因 :  默认情况下,事务只有在遇到RuntimeException异常或者Error的时候才会回滚
  • 指定了rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName属性,但是winner是noRollbackFor、noRollbackForClassName属性指定的异常
  • 指定了rollbackFor、rollbackForClassName、noRollbackFor、noRollbackForClassName属性,但是未推断出winner,异常也不是RuntimeExceptionError

需要注意的是,如果事务里面存在嵌套事务,需要将嵌套事务的方法catch一下,不然嵌套事务抛出的异常也可能导致上层事务的回滚

6.多线程调用

demo演示

@Service
public class FirstService {
    
    @Autowired
    private SecondService secondService;

    @Transactional
    public void doMainAndAsync() {
        saveData();
        updateData();
        new Thread(() -> secondService.doAsync()).start();
    }
}
@Service
public class SecondService {

    @Transactional
    public void doAsync() {
        System.out.println("doAsync");
    }
}

相关源码SqlSessionInterceptor#invoke

Spring是通过事务同步管理器(TransactionSynchronizationManager)来管理事务的,每一个线程都有一个独立的副本,所以不同线程取出来的sqlSession是不一样的,所以不能回滚异步线程的事务

7.其他

表不支持事务 : myisam引擎的表不支持事务

未被Spring管理 : 类上遗漏@Service注解

  • 27
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值