Sugus

V1

2022/09/27阅读:28主题:绿意

使用了@Transactional注解为什么事务未生效?

事务什么时候失效,你真的理解吗?相信很多小伙伴在面试过程经常会被面试官问到?@Transactional什么时候会失效?带着这个问题,我先从以下四个失效的例子,讲述失效的场景,先让我们初步有个大概的了解,然后再通过源码的解读,分析@Transactional失效的原因,从而避免以后在工作中踩坑。

@Transactional 失效场景

  1. 私有方法(访问权限修饰符为private的方法)
  2. 异常类型不匹配
  3. 同类非事务方法调用事务方法
  4. 多线程

示例1:私有方法

使用private修饰@Transactional注解的方法,由于idea会检查实现类方法的访问权限修饰符,所以此处不进行测试。

示例2:异常类型不匹配

这是由于我们在写@Transactional后没有设置回滚类型导致,这个在idea中会有黄色警告线标识-方法【exceptionType】需要在Transactional注解指定rollbackFor或者在方法中显式的rollback。提示我们指定回滚异常类型,避免产生意料之外的结果。

@Override
@Transactional
public void exceptionType(boolean isEnableRollbackException) throws IOException {
    userMapper.insert(User.builder().name("cpz").age(18).email("cpz@qq.com").build());
    if (isEnableRollbackException) {
        throw new RuntimeException("RUNTIME Exception");
    } else {
        throw new IOException("IOE Exception");
    }
}

代码解析:本例通过向数据库新增一条用户数据,然后抛出不同类型的异常(通过布尔类型参数isEnableRollbackException控制),最后观察该条数据是否在数据库里,即可完成类型异常的测试。

示例3:同类非事务方法调用事务方法

这是由于@Transactional是基于 aop 实现的,而 aop 使用动态代理实现,通过代理直接调用方法,会在方法前后加上事务相关逻辑。而现在直接通过类内部方法调用,则不会在方法前后生成事务相关逻辑,自然而然事务也会失效。

@Override
public void internalMethod() {
    this.insertUser();
    throw new RuntimeException("Runtime Exception");
}

@Transactional
public void insertUser() {   
    userMapper.insert(User.builder().name("cpz").age(18).email("cpz@qq.com").build());
}

代码解析:通过声明事务方法insertUser,而后使用同类方法internalMethod直接调用,从而绕过动态代理对象的增强-开启事务,提交事务等逻辑。最后观察该条数据是否在数据库里,即可完成同类非事务方法调用事务方法的测试。

示例4:多线程

由于父子线程是相对独立的,因此它们的之间的事务不是同一个的,所以不管是父线程抛出异常还是在子线程中抛出异常,对于另外的线程是不受影响的。

@Override
public void multiThread(boolean isThrowFromParentThread) {
    if (isThrowFromParentThread) {
        new Thread(() -> {
            userMapper.insert(User.builder().name("cpz").age(18).email("cpz@qq.com").build());
        }).start();
        throw new RuntimeException();
    } else {
        new Thread(() -> {
            userMapper.insert(User.builder().name("cpz").age(18).email("cpz@qq.com").build());
            throw new RuntimeException();
        }).start();
    }
}

代码解析:通过参数isThrowFromParentThread控制是父子线程抛出异常。最后观察该条数据是否在数据库里,即可完成多线程方法的测试。

源码解读:

1、ReflectiveMethodInvocation#proceed()

首先会经过一个拦截器interceptorOrInterceptionAdvice(TransactionInterceptor),执行invoke(this)方法,this为ReflectiveMethodInvocation对象实例,里面包含我们测试的代理对象,方法、参数等属性。

TransactionInterceptor#invoke(invocation)

2、执行方法之后会进入事务方法invokeWithinTransaction,此处是我们事务的核心,包含:1、获取事务属性后开启事务、2、目标方法的调用、3、事务的回滚、4、事务的提交,对示例的事务失效问题会在这里解惑。

第一步获取事务属性会调用computeTransactionAttribute方法完成属性的获取

里面会去判断是否为public修饰的方法,所以这也是示例1私有方法不行的原因

接下来看看第三步事务的回滚

回滚事务条件

而默认只有运行时异常(RuntimeException)和错误(ERROR)会回滚

所以示例2不配置rollbackFor = Exception.class会出现异常类型不匹配情况从而导致事务失效。

这里介绍下异常类的层次

「学习交流」

可以扫下面二维码,关注「我的极简博客」公众号。

一直在追求思路的传递而非代码的COPY

分类:

后端

标签:

Java

作者介绍

Sugus
V1