你的@Transaction注解失效了吗?

Posted on 2020-03-28 17:58  守护锁链  阅读(332)  评论(0编辑  收藏

引言

在项目中我们会经常使用@Transaction注解来进行事务控制,但是有时莫名没有这个事务控制没有起作用,在面试的时候,面试官也可能会问@Transaction注解在什么情况下会失效?今天我正好看到有一篇文章写到关于这个,所以就整理一下.

一.事务

我们知道在spring框架中要进行事务控制,我们有两种方式.一是【编程式事务控制】,二是【声明式事务控制】。

编程式事务控制

概念:顾名思义,就是我们可以通过编程代码的方式进行事务控制

缺点:代码侵入性强

实例:

try {
    //TODO something
     transactionManager.commit(status);
} catch (Exception e) {
    transactionManager.rollback(status);
    throw new InvoiceApplyException("异常失败");
}

声明式事务控制

概念:基于AOP面向切面,它将具体业务与事务处理部分解耦。

优点:事务控制的代码和我们具体业务的代码解耦,代码侵入性小。所以在实际开发过程中,这种方式也是使用最多。声明式事务也有两种方式,一是基于TX和AOP的xml配置文件方式,二是基于@Transaction注解

二、@Transaction介绍

  1.@Transaction注解可以使用的地方

    @Transaction可以作用在接口,类,类方法中

  • 作用类:该类的public方法都配置相同的事务属性信息.
  • 作用类方法:当类配置了@Transaction,方法也配置@Transaction,方法的事务会覆盖类的事务配置信息
  • 作用接口:不推荐这种方法,因为一旦标注在Interface上并且配置了Spring AOP使用CGLib动态代理,将会导致@Transaction注解失效  
@Transactional
 @RestController
 @RequestMapping
 public class MybatisPlusController {
     @Autowired
     private CityInfoDictMapper cityInfoDictMapper;
 
     @Transactional(rollbackFor = Exception.class)
     @GetMapping("/test")
    public String test() throws Exception {
        CityInfoDict cityInfoDict = new CityInfoDict();
        cityInfoDict.setParentCityId(2);
        cityInfoDict.setCityName("2");
        cityInfoDict.setCityLevel("2");
        cityInfoDict.setCityCode("2");
        int insert = cityInfoDictMapper.insert(cityInfoDict);
        return insert + "";
    }
}

2.@Transaction的属性

①propagation

propagation 代表事务的传播行为.默认值Propagation.REQUIRED.

 

Propagation.REQUIRED:如果当前存在事务,则加入该事务,如果当前不存在事务,则创建一个新的事务.(也就是说如果A方法和B方法都添加了注解,在默认传播模式下,A方法内部调用B方法,会把两个方法事务合并一个事务)

Propagation.SUPPORTS:如果当前存在事务,则加入该事务;如果当前不存在事务,则以非事务的方式继续运行

Propagation.MANDATORY:如果当前存在事务,则加入该事务,如果当前不存在事务,则抛出异常.

Propagation.REQUIRES_NEW:重新创建一个新的事务,如果当前存在事务,暂停当前的事务.(当前A中的a方法默认Propagation.REQUIRED模式,类B中的b方法加上采用Propagation.REQUIRES_NEW模式,然后在a方法中调用b方法操作数据库,然而a方法抛出异常后,b方法并没有进行回滚,因为Propagation.REQUIRES_NEW会暂停a方法的事务)

Propagation.NOT_SUPPORTED:以非事务的方式运行,如果当前存在事务,暂停当前的事务

Propagation.NEVER:以非事务的方式运行,如果当前存在事务,则抛出异常

Propagation.NESTED:和Propagation.REQUIRED效果一样.

②isolation属性

isolation:事务的隔离级别

③timeout属性

事务的超时时间,默认值为-1.如果超过该时间限制但事务还没有完成,则自动回滚.

④readOnly属性

指定事务是否为只读事务,默认为false;为了忽略哪些不需要事务的方法,比如读取数据,可以设置为true

⑤rollbackFor属性

用于指定能够触发事务回滚的异常类型,可以指定多个异常类型

⑥noRollbackFor属性

抛出指定的异常类型,不会改事务,也可以指定多个异常类型.

三、@Transaction失效的场景

①应用到非public修饰的方法上.之所以会失效是因为Spring AOP代理是,如图所示,TransactionInterceptor(事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy的内部类)的intercept方法或者JDKDynamicAopProxy的invoke方法会间接调用AbstractFallbackTransactionAttributeSource的computeTransactionAttribute方法,获取Transaction注解的事务配置信息.

protected TransactionAttribute computeTransactionAttribute(Method method,
    Class<?> targetClass) {
        // Don't allow no-public methods as required.
        if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
        return null;
}

此方法会检查目标方法的修饰符时候为public,不是public则不会获取@Transaction的属性配置信息.

②注解属性propagation属性错误.

如果配置propagation属性=下面三个属性值,则不会进行事务回滚.

TransactionDefinition.PROPAGATION_SUPPORTS

TransactionDefinition.PROPAGATION_NOT_SUPPORTED

TransactionDefinition.PROPAGATION_NEVER

③rollbackFor设置错误

rollbackFor可以指定能够处方事务回滚的异常类型.Spring默认抛出了未检查unchecked异常(继承自RuntimeException的异常)或者Error才回滚事务;其他异常不会处方回滚事务,如果在事务中抛出其他类型的异常,但却期望Spring能够回滚事务,就需要指定rollbackFor属性.

若在目标方法中抛出的异常时rollbackFor指定的异常的子类,事务同样会回滚.

private int getDepth(Class<?> exceptionClass, int depth) {
         if (exceptionClass.getName().contains(this.exceptionName)) {
             // Found it!
             return depth;
 }
         // If we've gone as far as we can go and haven't found it...
         if (exceptionClass == Throwable.class) {
             return -1;
}
return getDepth(exceptionClass.getSuperclass(), depth + 1);
}

④同一个类中方法调用,导致@Transaction失效

比如有一个类Test,它的一个方法A,A再调用本类的方法B(无论方法B是public还是private修饰),当方法没有声明注解事务,而B方法有,则尾部调用方法A之后,方法B的事务是不会起作用的.这是因为使用Spring AOP代理造成的,因为只有当事务方法被当前类以为的代码调用时,才会有spring生成的代理对象来管理.

⑤异常被你的catch"吃了"导致@Transaction失效.

这样情况是最常见的一种@Transaction注解失效场景

     @Transactional
     private Integer A() throws Exception {
         int insert = 0;
         try {
             CityInfoDict cityInfoDict = new CityInfoDict();
             cityInfoDict.setCityName("2");
             cityInfoDict.setParentCityId(2);
             /**
              * A 插入字段为 2的数据
             */
            insert = cityInfoDictMapper.insert(cityInfoDict);
            /**
             * B 插入字段为 3的数据
             */
            b.insertB();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

如果B方法内部抛了异常,而A方法此时try catch了B方法的异常,那这个事务还能正常回滚吗?

答案:不能!

会抛出异常:org.springframework.transaction.UnexpectedRollbackException: Transaction rolled back because it has been marked as rollback-only

因为当ServiceB中抛出了一个异常以后,ServiceB标志当前事务需要rollback.当时ServiceA中由于你手动的捕获这个异常并进行处理,ServiceA认为当前事务应该正常commit.此时就会出现了前后不一致,也就是这样,抛出了前片UnexceptedRollbackException异常,

Spring事务是在调用业务方法之前开始的,业务方法执行完毕之后才执行commit或者rollback,是否是有执行取决于是否抛出runTime异常.如果抛出runTimeException并在你的业务方法中没有catch到的话,事务会回滚.

在业务方法中一般不需要catch异常,如果非要catch一定要抛出throw new RuntimeException(),或者注解中指定抛异常类型@Transactional(rollbackFor=Exception.class),否则会导致事务失效,数据commit造成数据不一致,所以有些时候try catch反倒会画蛇添足。

数据库引擎不支持事务

这种情况出现的概率并不高,事务能否生效数据库引擎是否支持事务是关键。常用的MySQL数据库默认使用支持事务的innodb引擎。一旦数据库引擎切换成不支持事务的myisam,那事务就从根本上失效了。

以上取自【marcozheng】公众号,https://mp.weixin.qq.com/s/enKOM3F_Xxg123HPMCFUPw,并加以整理.

 

随笔 - 10, 文章 - 0, 评论 - 0

Copyright © 2020 守护锁链
Powered by .NET 5.0.0 on Kubernetes