主线程的用户信息,到子线程怎么丢了
作者:鱼仔
博客首页: https://codeease.top
公众号:Java鱼仔
# 前言
前几天有人问了我这样一个问题:在使用多线程的时候,发现有一些数据会在进入到子线程之后丢失,比如用户信息,又比如记录日志的TraceId等等。这个子线程数据丢失的问题我早前也遇到过,刚好来讲讲解决方案。
# 前期准备
首先通过一个案例来复现数据丢失的问题,在项目开发中,我们会将用户信息上下文放在一个ThreadLocal中
public class UserContext {
private static final ThreadLocal<UserContextInfo> userContext = new ThreadLocal<>();
private UserContext() {
// Private constructor to prevent instantiation
}
public static void setUserContext(UserContextInfo userContextInfo) {
userContext.set(userContextInfo);
}
public static UserContextInfo getUserContext() {
return userContext.get();
}
public static void clear() {
userContext.remove();
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
其中UserContextInfo这个类中存储了用户对象,比如下面这样:
@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserContextInfo {
private String userId;
private String username;
private String role;
}
2
3
4
5
6
7
8
然后在拦截器中,在接口刚进来时获取请求中的token信息,解析成用户上下文,使得之后在业务代码中可以直接使用
@Component
public class UserInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (handler instanceof HandlerMethod){
// 从请求中获取token,通过token拿到用户信息
String token = request.getHeader("User-Account");
UserContextInfo userContextInfo = getUserInfoByToken(token);
// 将用户账号存储到 ThreadLocal 中,先清空,再设置值
UserContext.setUserContext(userContextInfo);
return true;
}
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
UserContext.clear();
}
/**
* 模拟Token解析
* @param token
* @return
*/
private UserContextInfo getUserInfoByToken(String token) {
UserContextInfo userContextInfo = new UserContextInfo();
userContextInfo.setUserId("0001");
userContextInfo.setUsername("神秘的鱼仔");
userContextInfo.setRole("admin");
return userContextInfo;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
最后注册拦截器,这个用户上下文的功能就实现了。
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(userInterceptor);
}
}
2
3
4
5
6
7
8
9
10
当在写业务代码的时候,任何地方需要用到用户信息,都可以通过下面这行代码获取到用户上下文:
UserContextInfo userContext = UserContext.getUserContext();
# Bug复现
现在有这样一段代码,在主线程中使用了用户上下文信息,然后调用了一个异步方法:
@GetMapping("/testThreadLocal")
public void testThreadLocal(){
// 前面有一堆逻辑
// 获取用户信息
UserContextInfo userContext = UserContext.getUserContext();
System.out.println(userContext);
// 调用一个异步方法
testService.asyncMethod();
}
2
3
4
5
6
7
8
9
10
异步方法是这样的:
@Service
@Slf4j
public class TestService {
@Async("taskExecutor") // 指定使用自定义的线程池
public void asyncMethod() {
// 前面有一堆逻辑
// 异步执行的方法体
UserContextInfo userContext = UserContext.getUserContext();
System.out.println(userContext);
// 后面有一堆逻辑
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这个异步的线程池是这样的
@Configuration
public class ThreadPoolExecutorConfig {
private static final int CORE_THREAD_SIZE = Runtime.getRuntime().availableProcessors() + 1;
private static final int MAX_THREAD_SIZE = Runtime.getRuntime().availableProcessors() * 2 + 1;
private static final int WORK_QUEUE = 1000;
private static final int KEEP_ALIVE_SECONDS = 60;
@Bean("taskExecutor")
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_THREAD_SIZE);
executor.setMaxPoolSize(MAX_THREAD_SIZE);
executor.setQueueCapacity(WORK_QUEUE);
executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
executor.setThreadNamePrefix("task-thread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
executor.initialize();
return executor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
但是在结果输出的时候,发现异步线程中的这个用户信息不见了:
# Bug分析
其实这个问题的关键还是在于ThreadLocal,用户信息是存储在ThreadLocal中的,而ThreadLocal是线程内部的一份缓存数据,当使用了多线程之后,在其他线程中这份数据是不存在的,所以在子线程中无法取得用户上下文信息。 同样的问题还存在于一些主流的日志框架,在用TraceId跟踪日志的时候会发现跨线程后TraceId丢失了,也是因为ThreadLocal导致。
# Bug解决方法
在定义线程池Executor时,其实可以发现Excutor中有个变量叫做TaskDecorator,这是线程池的一个装饰器,能在线程任务执行前后添加一些自定义逻辑。
因此我们就可以通过线程装饰器来解决上面的问题,定义一个自己的装饰器:
public class BusinessContextDecorator implements TaskDecorator {
@Override
public Runnable decorate(Runnable runnable) {
UserContextInfo userContext = UserContext.getUserContext();
return () -> {
try {
UserContext.setUserContext(userContext);
runnable.run();
}finally {
UserContext.clear();
}
};
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
使用起来很简单,就是把用户信息拿出来,然后在子任务执行前将用户信息塞到子线程的上下文对象中,这样在其他子线程中也能使用,并且一劳永逸。 最后在线程池的配置中将这样配置加上去:
@Configuration
public class ThreadPoolExecutorConfig {
private static final int CORE_THREAD_SIZE = Runtime.getRuntime().availableProcessors() + 1;
private static final int MAX_THREAD_SIZE = Runtime.getRuntime().availableProcessors() * 2 + 1;
private static final int WORK_QUEUE = 1000;
private static final int KEEP_ALIVE_SECONDS = 60;
@Bean("taskExecutor")
public Executor taskExecutor(){
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(CORE_THREAD_SIZE);
executor.setMaxPoolSize(MAX_THREAD_SIZE);
executor.setQueueCapacity(WORK_QUEUE);
executor.setKeepAliveSeconds(KEEP_ALIVE_SECONDS);
executor.setThreadNamePrefix("task-thread-");
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
// 新增线程装饰器
executor.setTaskDecorator(new BusinessContextDecorator());
executor.initialize();
return executor;
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
# 总结
在这篇文章中,我们看到了一个由ThreadLocal引发的子线程中部分数据丢失的问题,所有存储在ThreadLocal中的数据,比如用户上下文,日志TraceId等,在跨线程之后都会丢失。
对于这个丢失的问题,我们可以使用线程装饰器TaskDecorator来解决。