@Transactional 注解又失效了?看完这个,你将不会抓狂

145 阅读6分钟

大家好,我是一名CV工程师。

作为一个Java开发程序员,或者说作为一个Springer或者SpringBooter。应该很少有人手动管理事务(显式使用START TRANSACTION;COMMIT;ROLLBACK;开启、提交、回滚事务)。99%都是将需要事务的方法上加上@Transactional注解,将事务交由Spring管理。

这样做既使得代码变得美观还减少了代码的耦合度,让程序员更专注于业务的开发。但同时也让问题的排查变得更困难。比如我明明在方法上加了这个注解,为什么方法出现异常了,数据库的更新操作并没有回滚。一旦出现这样的问题,很多人就会毫无头绪。那么接下来我将为大家具体分析@Transactional注解的几大原因以及解决方案。

在分析原因之前,我觉得大家还是有必要了解下Spring的声明式事务的实现原理和@Transactional注解的各个属性以及属性的作用。

Spring声明式事务的实现原理

@Transactional是基于aop实现的,那么必定有拦截器和切面。TransactionInterceptor会拦截加了@Transactional注解的方法,在调用目标方法前,会调用TransactionAspectSupportinvokeWithinTransaction方法对目标方法进行增强--对目标方法执行前,创建事务和数据库连接,在目标方法执行后,提供事务的提交和回滚。

@Transactional注解属性

package org.springframework.transaction.annotation;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Transactional {

    // 指定事务管理器的名称,当有多个事务管理器时需要指定
    @AliasFor("transactionManager")
    String value() default "";
    
    //同 value
    @AliasFor("value")
    String transactionManager() default "";
    
    //事务传播行为,默认是REQUIRED
    Propagation propagation() default Propagation.REQUIRED;
    
    //事务隔离级别,默认是DEFAULT(使用数据库默认隔离级别)
    Isolation isolation() default Isolation.DEFAULT;
    
    //事务超时时间,单位秒,默认是-1(使用数据库默认超时设置)
    int timeout() default -1;
    
    //是否只读事务,默认false
    boolean readOnly() default false;
    
    //指定哪些异常会触发事务回滚
    Class<? extends Throwable>[] rollbackFor() default {};
    
    //指定哪些异常类名会触发事务回滚
    String[] rollbackForClassName() default {};
    
    //指定哪些异常不会触发事务回滚
    Class<? extends Throwable>[] noRollbackFor() default {};
    
    //指定哪些异常类名不会触发事务回滚
    String[] noRollbackForClassName() default {};
}

我们对声明式事务的实现原理以及@Transactional注解属性有了基本了解之后,我们再来看有哪些场景会导致注解失效,就可以很快的理解其失效原因了。

一、方法内自调用

这是@Transactional注解失效的最常见原因。从上面Spring声明式事务的实现原理我们了解到是基于SpringAOP实现的。当在同一个类中的方法自调用时,不会使用Java反射调用方法,所以事务也就不会生效。

@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    public void update2(){
        this.update();
    }

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = Exception.class)
    public void update() {
        test1Mapper.updateId1ColTo2();
        throw new RuntimeException("模拟异常");
    }
}

以上这段代码,当在controller中直接调用update方法时,更新操作可以正常被回滚。是当在controller中调用update2方法,在update2方法中再直接调用update方法,就不会回滚。

解决方案

  1. 注入自身代理对象
@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    //注入自身代理
    @Resource
    private Test1Service test1Service;

    public void update2(){
        //不使用this调用,而使用自身代理对象调用
        test1Service.update();
    }

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = Exception.class)
    public void update() {
        test1Mapper.updateId1ColTo2();
        throw new RuntimeException("模拟异常");
    }
}

2. 将方法移到另一个类中

这个就不累赘了,就是将需要事务管控的方法移到一个其他类中,再通过其他类的代理对象调用。

  1. 通过 ApplicationContext 获取代理
@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    @Resource
    private ApplicationContext applicationContext;

    public void update2(){
        //通过applicationContext获取当前类的代理对象
        Test1Service test1Service =
                applicationContext.getBean(Test1Service.class);
        test1Service.update();
    }

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = Exception.class)
    public void update() {
        test1Mapper.updateId1ColTo2();
        throw new RuntimeException("模拟异常");
    }
}

二、@Transactional注解作用在非public方法上

@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    @Resource
    private Test1Service test1Service;

    public void update2(){
        //虽然是使用自身代理调用同类的方法,但是更新依旧没有回滚
        test1Service.update();
    }

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = Exception.class)
    void update() {
        test1Mapper.updateId1ColTo2();
        throw new RuntimeException("模拟异常");
    }
}

因为分布式事务是基于AOP实现的,而AOP是基于代理和反射实现的,在TransactionInterceptor (事务拦截器)中的目标方法执行前后进行拦截,DynamicAdvisedInterceptor的拦截器方法或者JdkDynamicAopProxyinvoke方法会间接调用AbstractFallbackTransactionAttributeSourcecomputeTransactionAttribute方法,获取@Transactional注解的事务配置信息。此方法会检查目标方法的修饰符是否为public,而不是public则不会获取@Transactional的属性配置信息,但是不会报错。

感兴趣的朋友可以研究下Spring关于声明式事务的源码

这个解决办法也很简单,将方法权限修饰词改为public就可以了。

三、@Transactional注解propagation属性设置错误

@Transactional注解的propagation属性是用来配置该方法与调用方方法的事务的传播机制的,涉及情况比较多,我单独写了一篇关于Spring声明式事务的传播机制的介绍文章。

传送门:[ ](Spring声明式事务:@Transactional用起来简单,但传播机制你真的配明白了吗?在使用Spring声明式事务 - 掘金)

四、@Transactional注解rollbackFor或者noRollbackFor属性设置错误

不知道这两个属性作用的朋友往上滑滑,上面介绍了@Transactional所有属性的作用。

@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;
    
    @Transactional
    public void update() throws Exception {
        test1Mapper.updateId1ColTo2();//不会回滚
        throw new Exception("模拟异常");

@Transactional注解未指定rollbackFor的属性时,默认回滚所有的RuntimeException。其他异常比如IOException这类的检查时异常默认不会回滚。

@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = IOException.class, noRollbackFor = RuntimeException.class)
    public void update() {
        test1Mapper.updateId1ColTo2(); //不会回滚
        throw new RuntimeException("模拟异常");
    }
}

以上示例中,明确指定了回滚IOException以及RuntimeException不回滚,所以更新操作也不会被回滚。

五、在事务方法中使用 catch捕捉了异常,导致@Transactional失效

如果在方法中使用try-catch捕捉了出错的代码,那么异常也就会被消化掉,也不会触发回滚。

@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = Exception.class)
    public void update() {
        test1Mapper.updateId1ColTo2();//不会回滚
        try {
            throw new RuntimeException("模拟异常");
        } catch (Exception e) {
            log.info("出现模拟异常");
        }
    }
}

解决办法

这时候如果需要回滚的话,可以在catch中将异常重新抛出。

@Service("test1Service")
public class Test1Service {

    @Resource
    private Test1Mapper test1Mapper;

    /**
     * 调用方法
     */
    @Transactional(rollbackFor = Exception.class)
    public void update() {
        test1Mapper.updateId1ColTo2();
        try {
            throw new RuntimeException("模拟异常");
        } catch (Exception e) {
            log.info("出现模拟异常");
            //将异常再次抛出
            throw new BusinessException("自定义业务异常");
        }
    }
}

六、数据库引擎不支持事务

可以使用 SHOW ENGINES;命令查看MySQL支持事务的引擎。

image.png

可以看出来我的版本只有InnoDB引擎支持事务操作。

关注我,后续为大家带来更多知识讲解以及高频率的面经!!!