1.问题背景

最近在优化公司财务对账模块的功能。由于对账任务是一个耗时比较长的过程,所以准备启用异步任务对账,让财务人员在开启对账任务后可以进行别的操作,等对账任务结束后再来查看对账结果。
由于在对账过程中需要涉及到数据库表数据的查询和插入,为了保证数据一致性,整个对账过程被设计在一个事务中进行。
在功能开发完成后,进行测试时发现我添加的事务并没有生效,当方法出现异常时,操作过的数据库数据并不会被回滚。

2.问题原因分析

我是在异步任务中执行事务的,我的事务声明在方法上。以下是我的伪代码

public void startReRecoTask(ReRecoTaskRecord reRecoTaskRecord) {threadPool.submit(new Runnable() {@Overridepublic void run() {log.info("重新对账任务执行开始");try {executeReRecoTask(reRecoTaskRecord);} catch (Exception e) {log.error("重新对账任务失败", e);}log.info("重新对账任务执行结束");}});}@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW, rollbackFor = Exception.class)public void executeReRecoTask(ReRecoTaskRecord reRecoTaskRecord) throws ParseException {//执行对账逻辑,在方法里事务失效}

在网上搜索事务失效场景逐一排除原因。

  1. 类不是被spring管理的。首先排除,因为我的类上声明了@Service注解,并且已经被注入到spring容器中
  2. 事务方法不是public的。也被排除,显然我的方法是public方法
  3. 事务方法的异常被捕获了。也被排除,因为我的事务方法中没有catch任何异常
  4. 在同一个类中,方法进行了内部调用。也被排除了,我的事务方法是在我new出的Runnable类对象中进行调用的,显然不是在同一个类中进行了方法的内部调用。
  5. 数据库不支持事务。也被排除了,我的数据库是mysql,使用的存储引擎为InnoDB,该引擎是支持事务的。

经过思考发现,虽然我的异步任务是new出来的Runnable对象。但是我的任务是直接调用了事务方法,我的Runnable对象并没有被spring管理,因此在调用被@Transactional注解修饰的方法时,是直接调用了方法本身,没有调用被spring动态代理过方法,自然不会被spring进行事务管理。

3.解决方法

既然声明式事务无法满足我们的需求了,那么我们可以使用编程式事务来灵活控制我们的事务。
改造后的代码如下:

public void startReRecoTask(ReRecoTaskRecord reRecoTaskRecord) {threadPool.submit(new Runnable() {@Overridepublic void run() {log.info("重新对账任务执行开始");try {transactionTemplate.execute(new TransactionCallbackWithoutResult() {@Overrideprotected void doInTransactionWithoutResult(TransactionStatus status) {try {executeReRecoTask(reRecoTaskRecord);} catch (RuntimeException e) {//手动回滚事务status.setRollbackOnly();throw e;} catch (ParseException e) {//手动回滚事务status.setRollbackOnly();throw new RuntimeException(e);}}});} catch (Exception e) {log.error("重新对账任务失败", e);}log.info("重新对账任务执行结束");}});}

经过测试,改造后的代码事务是生效的,当在执行异步对账任务中出现异常时,改动过的数据会被自动回滚。

4.其他事务失效的场景

除了上述我排除的五种常见的spring事务失效场景外,在以下场景时,spring事务也会失效。

  1. 事务方法被final、static关键字修饰,被这两个关键字修饰的方法不能被继承重写,所以基于动态代理实现的spring事务自然不能生效
  2. 配置错误的 @Transactional 注解,事务被设置成了只读事务,但我们在事务中执行了写操作,自然读写事务不会生效
  3. 事务超时时间设置过短
  4. 使用了错误的事务传播机制
  5. rollbackFor属性配置错误,我们代码跑出了ExcaptionA,但是配置了rollbackFor=ExcaptionB.class,那么当异常发生时,事务自然不会被回滚

5.总结

虽然声明式事务使用起来很方便,对业务代码入侵很小,但是当我们使用不当时,就会使事务不生效。不生效的原因主要就分为两种:

1. 事务的属性配置不正确(事务类型、回滚异常、超时时间等等属性)
2. 调用事务方法时没有经过spring管理的对象进行调用

我们在遇到事务不生效的问题时可以从这两个角度进行分析。

当声明式事务无法满足我们的业务需求时,我们可以使用编程式事务来满足需求,编程式事务可以更灵活更细粒度的控制事务。