摘要:
@Transactional中使用工作线程需要解决三大问题:
@Transactional不支持多线程;@Transactional中的工作线程无法获取主线程修改;@Transactional中的工作线程会上下文丢失。 本文使用AOP+Spring事件给出解决这三大问题的一种实践。
事务注解 @Transactional
总做周知,在Spring Boot中可以很方便的使用事务,在函数或类上使用注解@Transactional即可。
另外一方面,对于高耗时和涉及网络IO的操作,为了不阻塞主线程,一般会使用工作线程处理高耗时和网络IO。
考虑下面一个业务场景:
web服务收到人员信息更新请求updatePerson,主要有下面两个业务操作
@Transactional
public void updatePerson(String personCode) {
// 1. 将新的人员信息更新的数据库
...
// 2. 将新的人员信息同步到Elasticsearch
esService.syncToEs(personCode);
}
其中,第一步只会更新数据库表t_person。而第二步同步到Elasticsearch是一个耗时的操作,它会读取多个表格,t_person,t_company,t_social_relation,然后整合成一个对象发送给ES建立索引。
现在为了支持实时更新功能,需要优化updatePerson函数的执行时间。那么需要将esService.syncToEs(personCode);放到工作线程中执行。
但是,这个操作面临三大问题:
@Transactional不支持多线程@Transactional中的工作线程无法获取主线程修改@Transactional中的工作线程会上下文丢失
@Transactional 中开启工作线程面临的问题
1. @Transactional 不支持多线程
Spring Boot 对@Transactional的说明是
This annotation commonly works with thread-bound transactions managed by org.springframework.transaction.PlatformTransactionManager, exposing a transaction to all data access operations within the current execution thread. Note: This does NOT propagate to newly started threads within the method.
也就是说@Transactional是thread-bound(线程绑定),只在当前线程有效。对工作线程中的操作,@Transactional并不做任何事务的保证(一致性,隔离性,原子性,持久性)。
那么,有聪明的小伙伴就会回答,在工作线程中执行esService.syncToEs(personCode);完全可以开一个新的事务,姑且称它为工作线程事务。人员信息更新到数据库和同步es是可以拆分成两个动作的,主线程事务和工作线程事务是两个独立的事务。
但是,事情没有那么简单。这会面临第二个问题:工作线程无法获取主线程修改。
2. @Transactional 中的工作线程无法获取主线程修改
由于工作线程是在主线程事务运行过程中开启,也就是说,主线程事务还未提交,工作线程事务已经开启。那么,工作线程中的事务是无法看到主线程事务的更改(假设数据库隔离级别在提交读及以上)。
因此,需要在主线程事务结束后才能开始工作线程事务。例如下面的改造
public void updatePerson(String personCode) {
// 1. 将新的人员信息更新的数据库
updatePersonMainProcess(persoCode);
// 2. 将新的人员信息同步到Elasticsearch
esService.syncToEs(personCode);
}
@Transactional
private void updatePersonMainProcess(String personCode) {
// 1. 将新的人员信息更新的数据库
...
}
public class EsService {
@Async
@Transactional
public void syncToEs(String personCode) {
// 2. 将新的人员信息同步到Elasticsearch
...
}
}
然而还有第三个问题:@Transactional 中的工作线程会上下文丢失
3. @Transactional 中的工作线程会上下文丢失
在Spring Boot 中有上下文,例如web请求的上下文RequestContextHolder,它的内部是使用了ThreadLocal来实现的,因此RequestContextHolder也是thread-bound。当开启工作线程时,会导致web请求的上下文丢失。
当然,RequestContextHolder也提供了可以子线程可继承的setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable)方法。在开启工作线程前,将已有的属性重新设置一遍.
RequestContextHolder.setRequestAttributes(currentRequestAttributes(), true)
但是,在线程池+RequestContextHolder.setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable)是无效的。
原因是RequestContextHolder的父子线程继承上下文是通过InheritableThreadLocal实现。但是,InheritableThreadLocal无法在线程池的线程中工作。
继续深究下去,是因为InheritableThreadLocal在子线程创建(实现时会延迟到get方法)时将父线程的上下文拷贝过来。而线程池中的线程是复用的(并不会重新创建线程),因此无法将父线程上下文拷贝过来。
解决方案
可以使用阿里巴巴开发的transmittable-thread-local包 Github地址 中的TransmittableThreadLocal来解决线程池下父子线程上下文传递的问题。
简易介绍:使用类TransmittableThreadLocal来保存值,并跨线程池传递。
示例代码:
TransmittableThreadLocal<String> context = new TransmittableThreadLocal<>();
// =====================================================
// 在父线程中设置
context.set("value-set-in-parent");
// =====================================================
// 在子线程中可以读取,值是"value-set-in-parent"
String value = context.get();
@Transactional 中优雅开启工作线程
总结@Transactional 中开启工作线程需要增加的操作
- 工作线程需要在主线程事务结束后才开始运行
- 需要把主线程的上下文传递到工作线程中
下面给出一个实践,利用AOP+Spring事件来开启工作线程。
- 实现一个Spring事件发送类,在被调用
publish发送事件时,不直接发送事件,而是将事件存在上下文中。在控制器Controller执行成功返回后,取出保存在上下文的事件,开启工作线程发送事件。
工作逻辑:
PersonService中不直接执行同步es,而是使用AfterRequestEventPublisher抛出人员信息变更事件AfterRequestEventPublisher实现了ApplicationEventPublisher接口,其publish方法重写为:把事件保存到上下文中AfterRequestEventPublisher做了切面,实现事后返回通知afterReturningAdvice,拦截所有控制器,这里开启工作线程发送事件。当控制器返回时,Spring调用事后返回通知AfterRequestEventPublisher.afterReturningAdvice。
public class PersonService {
// 自己实现的Spring事件通知器,负责在controller返回后启动工作线程发送事件
@Resource
private AfterRequestEventPublisher afterRequestEventPublisher;
@Transactional
public void updatePerson(String personCode) {
// 1. 将新的人员信息更新的数据库
...
// 2. 抛出事件,通知将新的人员信息同步到Elasticsearch
afterRequestEventPublisher.publish(new PersonEditEvent(PersonCode))
}
}
public class AfterRequestEventPublisher implements ApplicationEventPublisher {
// Spring boot 默认的事件通知
@Resource
private ApplicationEventPublisher publisher;
// 工作线程的线程池
@Resource
private ThreadPoolTaskExecutor threadPoolTaskExecutor;
/**
* 将事件收集起来,在请求结束的时候开启工作线程异步发送事件
*/
@Override
public void publishEvent(Object event) {
List<Object> eventObjects = (List<Object>) RequestContextHolder.currentRequestAttributes()
.getAttribute("AfterRequestEventPublisher", RequestAttributes.SCOPE_REQUEST);
if (Objects.isNull(eventObjects)) {
eventObjects = new ArrayList<>();
eventObjects.add(event);
RequestContextHolder.currentRequestAttributes()
.setAttribute("AfterRequestEventPublisher", eventObjects, RequestAttributes.SCOPE_REQUEST);
} else {
eventObjects.add(event);
}
}
/**
* 拦截所有controller控制器的所有方法,在controller结束的时候才抛出事件
*/
@AfterReturning(pointcut = "execution(public * org.example.controller.*Controller.*(..))", returning = "retVal")
public void afterReturningAdvice(Object retVal) {
final List<Object> eventObjects = (List<Object>) RequestContextHolder.currentRequestAttributes()
.getAttribute("AfterRequestEventPublisher", RequestAttributes.SCOPE_REQUEST);
if (CollectionUtils.isEmpty(eventObjects)) {
return;
}
// 上下文传递到工作线程,在工作线程中发送事件。这里不使用@Async的原因是留给未来做一些线程的自定义设置。例如用更优雅的上下文传递等。
// 这里要改成用 TransmittableThreadLocal
RequestContextHolder.setRequestAttributes(RequestContextHolder.currentRequestAttributes(), true);
threadPoolTaskExecutor.execute(() -> {
for (Object event : eventObjects) {
publisher.publishEvent(event);
}
});
}
}
public class EsService {
// 接收 PersonEditEvent 事件,在工作线程中执行
@EventListener(PersonEditEvent.class)
@Transactional
public void syncToEs(PersonEditEvent event) {
// 同步到es中
}
}
总结
Spring Boot的事务注解@Transactional中使用工作线程的前提条件:
-
主线程的流程可以拆分成两个独立的事务。
如果在本例中,如果同步es操作的逻辑时,只将变更部分的信息同步到es中,那么该操作是不能在工作线程中执行。因为工作线程事务不能感知主线程事务是否提交成功。如果主线程事务回滚,而工作线程依然把变更部分同步到es中,那么就造成了数据库和es不一致。同理,主线程成功提交,而工作线程回滚也会造成数据库与es不一致。
Spring Boot的事务注解@Transactional中使用工作线程会遇到的问题:
-
@Transactional不支持多线程;要求主线程和工作线程使用两个独立的事务。
-
@Transactional中的工作线程无法获取主线程修改;要求工作线程在主线程事务结束后才能开始执行。
-
@Transactional中的工作线程会上下文丢失。要求传递上下文。使用
transmittable-thread-local包 Github地址 中的TransmittableThreadLocal来保存值,并跨线程池传递。