业务上线后出现了一个bug: 用户配置/删除课程时,根据已配置课程数据生成栏目树时好时坏,具体表现为:某种时刻刷新的栏目树是数据未入库之前的旧数据。但是全量刷新类目接口稳定不报错,两个公用一个接口。
查询具体代码发现,刷新类目接口是在课程配置@Transactional中调用了@async调用的异步方法,分析,很有可能是这儿导致,数据未commit完毕,异步刷新类目方法已经调用执行完毕了。
其中stackoverflow上同样有人提出了相同的问题:
https://stackoverflow.com/questions/51833306/using-async-inside-a-transaction-in-spring-application
具体的解决办法,考虑有两种:
- 使用event事件机制,当数据全部更新完毕后通知异步方法执行,这是基于观察者模式
- 使用TransactionSynchronizationManager,重写其中的 afterCommit 方法,标明在数据commit完毕后执行。
具体伪代码实现如下:
public class CourseServiceImpl implements CourseService {
/**
* 配置课程
*/
public void addRealmCourseList(){
//更新数据库操作
update();
//调用异步方式
executeAfterTransactionCommits(()->{
//具体异步方法
...
});
}
/**
* commit之后执行
*/
private void executeAfterTransactionCommits(Runnable task) {
TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
@Override
public void afterCommit() {
task.run();
}
});
}
}
值得注意的是,除去这种异常case之外,@Transactional
与@Async
联合使用还会导致设置的Transaction注解不生效。原因其实也很简单:
- Spring 实现这两个注解的方式都是通过
AOP
。 - 在实现时,Async注解强制覆盖
AOP
的order为最小值(它认为Async应该是执行的AOP
链中的第一个advisor) - 但是在实现Transactional注解时,却没有覆盖order,这意味着它仍然为默认的Integer.MAX_VALUE,order可配置。所以异步切面会先于事务切面执行。
- 假设
@Transactional
能先于Async切面执行,但由于spring事务管理依赖的是ThreadLocal,所以在开启的异步线程里面感知不到事务,说细点就是在Spring开启事务之后,会设置一个连接到当前线程,但这个时候又开启了一个新线程,执行实际的SQL代码时,通过ThreadLocal获取不到连接就会开启新连接,也不会设置autoCommit,所以这个函数整体将没有事务。
其中,在spring源码中同样有人提到了这样一个issue,目前已经关闭:
https://github.com/spring-projects/spring-framework/issues/11806
由此可见,当我们业务中有需要Transaction与async同时使用时,一定要小心使用。
网友评论