相关文章推荐
英俊的茴香  ·  std::shuffle ...·  1 年前    · 
奔放的排球  ·  Python CGI 编程 | 菜鸟教程·  1 年前    · 
机灵的鸵鸟  ·  Exception during ...·  1 年前    · 
@Transactional千万不要这样用!!踩坑了你都可能发现不了!!!

@Transactional千万不要这样用!!踩坑了你都可能发现不了!!!

39ee-a8190c49d990912be28c50eee32fe5be


前阵子接手了一段同事之前的代码,里面用到了@transaction注解,了解Spring的小伙伴肯定知道,@Transactional是Spring提供的一种控制事务管理的快捷手段。但是我这段程序在运行的时候,经常出现莫名其妙的问题,连夜研究了好久才搞清楚,在这里记录一下, 避免大家入坑。

1. 大家来找茬

在介绍具体问题之前,我把问题代码简化了一下,看大家能找到其中的问题吗?

问题代码1

下面的这段代码主要是想利用MySQL里面的行锁 select for update ,来实现简单的分布式锁。但是在实践过程中,发现这个锁好像并没有生效,而且在数据库的里面也没有查找对应transaction连接的信息。

@Component
@EnableScheduling
public class someService {
  @Scheduled(...)
  public doSomeWork() {
    // find some id by logic
    // process the related info
    doOtherWork(id);
  @Transactional(isolation = Isolation.READ_COMMITTED)
  public void doOtherWork(id) {
    Info info = requestMapper.selectByPrimaryKeyForUpdate(id);
    doSomeFollowingProcess(info);
}

问题代码2

下面代码分两个步骤,第一步会检查相关信息,第二步调用了一个transactional修饰的方法,完成一些基本工作;但在实践中,发现一个非常诡异的问题,在MainWork中,doSomeCheck执行时会抛出nullPointException,debug发现所有autowired进来的service均为空,注释掉doSomeCheck里面的内容后,继续往下执行,却发现doWork能够正常执行,所有的注入均没有问题。

``` java @Component public class MainWork { @AutoWired DetailWork detailWork

public void workflow() { detailWork.doSomeCheck(); detailWork.doWork(); } }

@Component public class DetailWork {

@AutoWired
UsefulService usefulService;
@AutoWired
InfoService infoService;
@Transactional(isolation = Isolation.READ_COMMITTED)
public void doWork() {
  usefulService.doSomeWork();
void doSomeCheck() {
  infoService.getInfo();
}

}

大伙看看能发现什么问题吗?
## 2. 关于@Transactional注解
Spring支持**编程式事务管理**和**声明式事务管理**两种方式。
* 编程式事务管理使用TransactionTemplate或者直接使用底层的PlatformTransactionManager。
* 声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后,根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,**只需基于@Transactional注解的方式,便可以将事务规则应用到业务逻辑中**。
下图是调用@Transactional注解的方法时,Spring内部的时序图。简单来讲就是IOC容器初始化时,会生成@Transactional注解所在类的代理对象,然后实际执行中会通过AOP执行代理对象的方法,TransactionAdvisor会在方法调用前判断是否开启事务,在调用结束后,会判断是否提交或回滚事务。
![Eithaa](https://imgs.lfeng.tech/images/2023/02/Eithaa.png)
深入研究代码,我们会发现TransactionInterceptor (事务拦截器)在目标方法执行前后进行拦截,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 方法或 JdkDynamicAopProxy 的 invoke 方法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 方法,获取Transactional 注解的事务配置信息。
```java
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则不会获取@Transactional 的属性配置信息。也就是说protected、private 修饰的方法上使用 @Transactional 注解会导致事务无效。

了解了@Transactional的原理之后,我们在回头看看之前的问题,会不会是使用方法不对导致的呢?

3. 拨云见日

问题代码1解析

下面的代码中,我们在同一个类里面调用了@Transactional修饰的方法,其实这样调用的话并没有用到Spring AOP生成的代理对象。从上面的时序图也可以看到,只有当事务方法被当前类以外的代码调用时,才会由Spring生成的代理对象来管理。

@Component
@EnableScheduling
public class someService {
  @Scheduled(...)
  public doSomeWork() {
    // find some id by logic
    // process the related info
    doOtherWork(id);
  @Transactional(isolation = Isolation.READ_COMMITTED)
  public void doOtherWork(id) {
    Info info = requestMapper.selectByPrimaryKeyForUpdate(id);
    doSomeFollowingProcess(info);
}

那如何解决这种类内调用的问题呢? 很简单,可以 使用applicationContext直接从IOC容器中将someService类取出来,然后再调用doOtherWork方法即可,这样就能用上Spring AOP生成的代理对象了

下面是更改之后的代码,更改之后发现事务生效了,问题解决!

@Component
@EnableScheduling
public class someService {
  @Autowired
  private ApplicationContext applicationContext;
  @Scheduled(...)
  public doSomeWork() {
    // find some id by logic
    // process the related info
    SomeService someService = applicationContext.getBean(someService.class);
    someService.doOtherWork(id);
  @Transactional(isolation = Isolation.READ_COMMITTED)
  public void doOtherWork(id) {
    Info info = requestMapper.selectByPrimaryKeyForUpdate(id);
    doSomeFollowingProcess(info);
}

问题代码2解析

下面的代码中,MainWork调用doSomeCheck的时候,会出现null的情况,原因是由于该方法不是public方法,会导致@Transactional调用失败。你可能会说这就是普通方法,跟@Transactional有什么关系?

需要注意的是,无论transactional注解在类上还是在方法上,IOC容器都会生成对应类的代理对象,然后使用代理对象去访问对应的方法。在这个例子里面, 调用doWork时一切正常,事务也会生效;但是调用doSomeCheck时,从之前的分析可以看到,由于方法不是public,此时事务管理器不会起作用,直接导致所有的autowired未完成注入。修改的方法也很简单,把doSomeCheck改成public就行了。

这个问题隐藏比较深一些,不清楚原理很难发现这个问题。

@Component
public class MainWork {
  @AutoWired
  DetailWork detailWork
  public void workflow() {
    detailWork.doSomeCheck();
    detailWork.doWork();
@Component
public class DetailWork {
    @AutoWired
    UsefulService usefulService;
    @AutoWired
    InfoService infoService;
    @Transactional(isolation = Isolation.READ_COMMITTED)
    public void doWork() {
      usefulService.doSomeWork();