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

前阵子接手了一段同事之前的代码,里边用到了@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能够正常履行,一切的注入均没有问题。

@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会在办法调用前判别是否开启业务,在调用完毕后,会判别是否提交或回滚业务。

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

深入研究代码,咱们会发现TransactionInterceptor (业务阻拦器)在方针办法履行前后进行阻拦,DynamicAdvisedInterceptor(CglibAopProxy 的内部类)的 intercept 办法或 JdkDynamicAopProxy 的 invoke 办法会间接调用 AbstractFallbackTransactionAttributeSource的 computeTransactionAttribute 办法,获取Transactional 注解的业务装备信息。

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();
   }
 
    public void doSomeCheck() {
   infoService.getInfo();
   }
}

4. 相关拓展

几种业务失效的场景

上面说到的两个问题,其实便是@Transactional注解运用不当,导致失效的两种情形;除此之外,以下几种状况也会导致业务失效:

  • 业务代码中存在反常时,运用try…catch…句子块捕获,而catch句子块没有throw new RuntimeExecption反常;(最难被排查到问题且容易忽略)
  • 注解@TransactionalPropagation特点值设置过错即Propagation.NOT_SUPPORTED(一般不会设置此种传达机制)
  • mysql联系型数据库,且存储引擎是MyISAM而非InnoDB,则业务会不起作用(比较少见);
  • 业务代码抛出反常类型非RuntimeException,业务失效;Spring默许抛出未查看unchecked反常(继承自 RuntimeException 的反常)或许 Error才回滚业务;其他反常不会触发回滚业务。假如在业务中抛出其他类型的反常,但却期望 Spring 能够回滚业务,就需求指定 rollbackFor特点。

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

业务的传达行为

业务的传达行为也会影响到业务与业务之间的联系,一定要搞清楚,否则常常会呈现很古怪的问题。

具体来讲有以下几种特点:

  • 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 效果一样。

业务的阻隔等级

SQL规范界说了4种业务阻隔等级来防止3种数据不一致的问题。业务等级从高到低,分别为:

1.Serializable(序列化

体系中一切的业务以串行地办法逐一履行,所以能防止一切数据不一致状况。

可是这种以排他办法来操控并发业务,串行化履行办法会导致业务排队,体系的并发量大幅下降,运用的时候要肯定稳重。

2.Repeatable read(可重复读)

一个业务一旦开始,业务过程中所读取的一切数据不允许被其他业务修正。

一个阻隔等级没有办法处理“幻影读”的问题。

由于它只“保护”了它读取的数据不被修正,可是其他数据会被修正。假如其他数据被修正后刚好满足了当时业务的过滤条件(where句子),那么就会产生“幻影读”的状况。

其他两种业务阻隔等级为:

3.Read Committed(已提交读)

一个业务能读取到其他业务提交过(Committed)的数据。

一个业务在处理过程中假如重复读取某一个数据,而且这个数据刚好被其他业务修正并提交了,那么当时重复读取数据的业务就会呈现同一个数据前后不同的状况。

在这个阻隔等级会产生“不可重复读”的场景。

4.Read Uncommitted(未提交读)

一个业务能读取到其他业务修正过,可是还没有提交的(Uncommitted)的数据。

数据被其他业务修正过,但还没有提交,就存在着回滚的可能性,这时候读取这些“未提交”数据的状况便是“脏读”。

在这个阻隔等级会产生“脏读”场景。


参阅:

  • www.huaweicloud.com/zhishi/edu-…
  • /post/707815…

更多技能原创分享,欢迎关注【后端精进之路】了解~