51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

主线程的用户信息,到子线程怎么丢了

# 前言 {#前言}

前几天有人问了我这样一个问题:在使用多线程的时候,发现有一些数据会在进入到子线程之后丢失,比如用户信息,又比如记录日志的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(); }

}

其中UserContextInfo这个类中存储了用户对象,比如下面这样:

@Data
@AllArgsConstructor
@NoArgsConstructor
public class UserContextInfo {
    private String userId;
    private String username;
    private String role;
}

然后在拦截器中,在接口刚进来时获取请求中的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(&quot;0001&quot;); userContextInfo.setUsername(&quot;神秘的鱼仔&quot;); userContextInfo.setRole(&quot;admin&quot;); return userContextInfo; }

}

最后注册拦截器,这个用户上下文的功能就实现了。

@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private UserInterceptor userInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(userInterceptor);
}

}

当在写业务代码的时候,任何地方需要用到用户信息,都可以通过下面这行代码获取到用户上下文:

UserContextInfo userContext = UserContext.getUserContext();

# Bug复现 {#bug复现}

现在有这样一段代码,在主线程中使用了用户上下文信息,然后调用了一个异步方法:

@GetMapping("/testThreadLocal")
public void testThreadLocal(){
    // 前面有一堆逻辑
    // 获取用户信息
    UserContextInfo userContext = UserContext.getUserContext();
    System.out.println(userContext);
// 调用一个异步方法
testService.asyncMethod();

}

异步方法是这样的:

@Service
@Slf4j
public class TestService {
@Async(&quot;taskExecutor&quot;) // 指定使用自定义的线程池
public void asyncMethod() {
    // 前面有一堆逻辑
    // 异步执行的方法体
    UserContextInfo userContext = UserContext.getUserContext();
    System.out.println(userContext);
    // 后面有一堆逻辑
}

}

这个异步的线程池是这样的

@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(&quot;taskExecutor&quot;) 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(&quot;task-thread-&quot;); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); executor.initialize(); return executor; }

}

但是在结果输出的时候,发现异步线程中的这个用户信息不见了:

# Bug分析 {#bug分析}

其实这个问题的关键还是在于ThreadLocal,用户信息是存储在ThreadLocal中的,而ThreadLocal是线程内部的一份缓存数据,当使用了多线程之后,在其他线程中这份数据是不存在的,所以在子线程中无法取得用户上下文信息。 同样的问题还存在于一些主流的日志框架,在用TraceId跟踪日志的时候会发现跨线程后TraceId丢失了,也是因为ThreadLocal导致。

# Bug解决方法 {#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();
            }
        };
    }
}

使用起来很简单,就是把用户信息拿出来,然后在子任务执行前将用户信息塞到子线程的上下文对象中,这样在其他子线程中也能使用,并且一劳永逸。 最后在线程池的配置中将这样配置加上去:

@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(&quot;taskExecutor&quot;) 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(&quot;task-thread-&quot;); executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy()); // 新增线程装饰器 executor.setTaskDecorator(new BusinessContextDecorator()); executor.initialize(); return executor; }

}

# 总结 {#总结}

在这篇文章中,我们看到了一个由ThreadLocal引发的子线程中部分数据丢失的问题,所有存储在ThreadLocal中的数据,比如用户上下文,日志TraceId等,在跨线程之后都会丢失。

对于这个丢失的问题,我们可以使用线程装饰器TaskDecorator来解决。

赞(5)
未经允许不得转载:工具盒子 » 主线程的用户信息,到子线程怎么丢了