Code Ease Code Ease
  • 个人博客网站 (opens new window)
  • 好用的工具网站 (opens new window)
  • Java核心基础
  • 框架的艺术
  • 分布式与微服务
  • 开发经验大全
  • 设计模式
  • 版本新特性
数据库系列
大数据+AI
  • xxl-job
运维与Linux
  • 基于SpringBoot和BootStrap的论坛网址
  • 基于VuePress的个人博客网站
  • 基于SpringBoot开发的小功能
  • 做一个自己的IDEA插件
程序人生
关于我
  • 分类
  • 标签
  • 归档

神秘的鱼仔

你会累是因为你在走上坡路
  • 个人博客网站 (opens new window)
  • 好用的工具网站 (opens new window)
  • Java核心基础
  • 框架的艺术
  • 分布式与微服务
  • 开发经验大全
  • 设计模式
  • 版本新特性
数据库系列
大数据+AI
  • xxl-job
运维与Linux
  • 基于SpringBoot和BootStrap的论坛网址
  • 基于VuePress的个人博客网站
  • 基于SpringBoot开发的小功能
  • 做一个自己的IDEA插件
程序人生
关于我
  • 分类
  • 标签
  • 归档
服务器
  • Java核心基础

  • 框架的艺术

    • Spring

    • Mybatis

    • SpringBoot

      • 如何用SpringBoot(2.3.3版本)快速搭建一个项目
      • 一步步带你看SpringBoot(2.3.3版本)自动装配原理
      • SpringBoot配置文件及自动配置原理详解,这应该是SpringBoot最大的优势了吧
      • SpringBoot整合jdbc、durid、mybatis详解,数据库的连接就是这么简单
      • SpringBoot整合SpringSecurity详解,认证授权从未如此简单
      • SpringBoot整合Shiro详解,还在自己写登陆注册早落伍了
      • SpringBoot如何实现异步、定时任务?
      • 如何在SpringBoot启动时执行初始化操作,两个简单接口就可以实现
      • 如何使用SpringBoot写一个属于自己的Starter
      • SpringBoot请求日志,如何优雅地打印
      • 主线程的用户信息,到子线程怎么丢了
        • 前言
        • 前期准备
        • Bug复现
        • Bug分析
        • Bug解决方法
        • 总结
    • MQ

    • Zookeeper

    • netty

  • 分布式与微服务

  • 开发经验大全

  • 版本新特性

  • Java
  • 框架的艺术
  • SpringBoot
CodeEase
2024-07-30
目录

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

作者:鱼仔
博客首页: codeease.top (opens new window)
公众号: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();
    }
}
1
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;
}
1
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;
    }
}

1
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);
    }
}
1
2
3
4
5
6
7
8
9
10

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

UserContextInfo userContext = UserContext.getUserContext();
1

# Bug复现

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

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

    // 调用一个异步方法
    testService.asyncMethod();
}
1
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);
        // 后面有一堆逻辑
    }

}
1
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;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

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

11-1.png

# 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();
            }
        };
    }
}

1
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;
    }
}
1
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来解决。

上次更新: 2025/04/29, 17:22:06
SpringBoot请求日志,如何优雅地打印
RabbitMQ的了解安装和使用

← SpringBoot请求日志,如何优雅地打印 RabbitMQ的了解安装和使用→

最近更新
01
AI大模型部署指南
02-18
02
半个月了,DeepSeek为什么还是服务不可用
02-13
03
Python3.9及3.10安装文档
01-23
更多文章>
Theme by Vdoing | Copyright © 2023-2025 备案图标 浙公网安备33021202002405 | 浙ICP备2023040452号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式