记一次ThreadLocal中的用户信息混乱问题

threadlocal · 浏览次数 : 18

小编点评

**问题记录测试同学在项目合同签约完以后,发现签约完成后数字资产的 Owner 被改变了。** **问题分析:** 1. ThreadLocal 没有及时清理导致数据不一致。 2. 由于内存泄漏,ThreadLocal 中的用户信息在业务逻辑结束时没有被清理。 3. 在消息监听方法里获取用户信息时,ThreadLocal 中的用户信息可能仍然不为空。 **解决方案:** 1. 在业务逻辑结束时清理 ThreadLocal 中的用户信息。 2. 在消息监听方法里确保在获取用户信息之前清理 ThreadLocal 信息。 3. 使用`remove()`方法清理 ThreadLocal ,并在方法执行结束时进行清理操作。 4. 在消息监听方法里确保获取到的用户信息是`null`。 **代码示例:** ```java public class CurrentUserUtil { private static final ThreadLocal userThreadLocal = new ThreadLocal<>(); public static void addCurrentUser(User user) { userThreadLocal.set(user); } public static User getCurrentUser() { return userThreadLocal.get(); } public static void remove() { userThreadLocal.remove(); } } public class Interceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { User user = new User(); user.setWorkNo("123456"); CurrentUserUtil.addCurrentUser(user); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { UserInfoThreadHolder.remove(); } } ```

正文

前言

记录一次开发中遇到的关于 ThreadLocal 问题,场景是数据库表中的操作人总是无缘无故的被更改,排查了几遍代码才发现是 ThreadLocal 没有及时清理导致的。


一、为什么使用 ThreadLocal

1. ThreadLocal 的好处

一般的项目设计开发中,用户登录后,我们会将用户的信息存到 Session,如果想在其它地方获取用户信息,需要频繁的传递 Session 对象。如果遇到高并发,多人同时登录系统时,可能会出现 Session 混乱,导致获取的用户信息不一致。


而 ThreadLocal 可以将用户信息保存在本地线程变量中,当线程结束后我们在把用户信息清理掉。这样在进行开发时,就可以很方便的从全局获取用户信息,不需要频繁的传递 Session 对象,提升开发效率。


并且 ThreadLocal 是线程隔离的,每个线程都有自己独立的变量副本,不会受到其他线程的影响,可以避免线程安全问题。

2. 注意事项

ThreadLocal 也是有缺点的,有两个比较突出的缺点是内存泄漏和上下文切换问题:

  • 内存泄漏:ThreadLocal 是与线程绑定的,如果线程一直存在,那么对应的变量副本也会一直存在,可能会占用大量的内存空间,如不及时清理 ,可能会导致内存泄漏问题。

  • 上下文切换问题:由于每个线程都有自己的变量副本,当需要在多个线程之间共享数据时,可能需要进行额外的上下文切换操作,增加了程序的复杂性和开销。

所以我们在业务逻辑结束时,一定要清理一下 ThreadLocal 的数据

3. 如何实现

不管过滤器还是拦截器,只要是能拦截住请求,获取到用户信息均可,这里简单介绍拦截器的实现方法。


创建用户信息工具类

public class CurrentUserUtil {

    /**
     * 初始化用户对象的ThreadLocal
     */
    private static final ThreadLocal<User> userThreadLocal = new ThreadLocal<>();

    /**
     * 添加当前登录用户方法
     */
    public static void addCurrentUser(User user){
        userThreadLocal.set(user);
    }

    public static User getCurrentUser(){
        return userThreadLocal.get();
    }


    /**
     * 防止内存泄漏
     */
    public static void remove(){
        userThreadLocal.remove();
    }

}

拦截器中调用,在preHandle()方法中根据业务需求将用户的登录信息存放之 ThreadLocal 中即可。然后最后记得清除相关数据以避免内存泄漏。

public class UnifyInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        User user = new User();
        user.setWorkNo("123456");
        CurrentUserUtil.addCurrentUser(user);
        return true;
    }

    /**
     * 避免内存泄露
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        
        UserInfoThreadHolder.remove();
    }
}


这里关于拦截器的详细使用不多做赘述,详细介绍见Spring 的过滤器和拦截器

二、问题记录

测试同学在项目合同签约完以后,发现签约完成后数字资产的 Owner 被改变了。然后去排查代码,开始并没有发现代码有问题,于是去排查 ThreadLocal 中的用户信息,发现是操作人已经不一致。所以在执行后续操作时数据库中的操作人就被修改了。


按理说,在设置用户信息之前第一次获取的值始终应该是 null,但是请求线程被 Tomcat 回收后,不一定会立即销毁,如果不在请求结束后主动 remove 线程中的 ThreadLocal 信息,可能会影响后续逻辑,拿到脏数据。


这里我还犯了一个小错误,我知道 ThreadLocal 会产生内存泄漏,需要进行清除操作,但是我把它放在方法执行前了,也就是说每次请求会现进行清除操作,正常流程是不会出错的。


但是当我们通过 MQ 消息队列接受消息时,按理说此时的 ThreadLocal 里的用户信息应该为 null,但由于没有在方法执行结束前及时清理 ThreadLocal,导致了用户信息出现了不一致的情况。后续我将问题 clear 方法放在了结束时执行,并在消息监听的方法里也进行了清理保证不会被其他用户信息所干扰。


所以,不论使用 ThreadLocal 存什么数据,请务必记得,在业务逻辑结束之前清理 ThreadLocal 中的数据。

与记一次ThreadLocal中的用户信息混乱问题相似的内容:

记一次ThreadLocal中的用户信息混乱问题

记录一次开发中遇到的关于 ThreadLocal 问题,场景是数据库表中的操作人总是无缘无故的被更改,排查了几遍代码才发现是 ThreadLocal 没有及时清理导致的。

记一次 CDN 流量被盗刷经历

先说损失,被刷了 70 多RMB,还好止损相对即时了,亏得不算多,PCDN 真可恶啊。 600多G流量,100多万次请求。 怎么发现的 先是看到鱼皮大佬发了一篇推文突发,众多网站流量被盗刷!我特么也中招了。 抱着看热闹的心情点开阅读了。。。心想,看看自己的中招没,结果就真中招了 。 被盗刷资源分

记一次 .NET某上位视觉程序 离奇崩溃分析

一:背景 1. 讲故事 前段时间有位朋友找到我,说他们有一个崩溃的dump让我帮忙看下怎么回事,确实有太多的人在网上找各种故障分析最后联系到了我,还好我一直都是免费分析,不收取任何费用,造福社区。 话不多说,既然有 dump 来了,那就上 windbg 说话吧。 二:WinDbg 分析 1. 为什么

记一次 .NET某酒业业务系统 崩溃分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他的程序每次关闭时就会自动崩溃,一直找不到原因让我帮忙看一下怎么回事,这位朋友应该是第二次找我了,分析了下 dump 还是挺经典的,拿出来给大家分享一下吧。 二:WinDbg 分析 1. 为什么会崩溃 找崩溃原因比较简单,用 !analyze -v 命

记一次aspnetcore发布部署流程初次使用k8s

主题: aspnetcorewebapi项目,提交到gitlab,通过jenkins(gitlab的ci/cd)编译、发布、推送到k8s。 关于gitlab、jenkins、k8s安装,都是使用docker启动服务。 首先新建一个项目,为了方便浏览就把swaggerr非开发环境不展示去掉 下面就是需

记一次 .NET某网络边缘计算系统 卡死分析

一:背景 1. 讲故事 早就听说过有什么 网络边缘计算,这次还真给遇到了,有点意思,问了下 chatgpt 这是干嘛的 ? 网络边缘计算是一种计算模型,它将计算能力和数据存储位置从传统的集中式数据中心向网络边缘的用户设备、传感器和其他物联网设备移动。这种模型的目的是在接近数据生成源头的地方提供更快速

记一次RocketMQ消费非顺序消息引起的线上事故

应用场景 C端用户提交工单、工单创建完成之后、会发布一条工单创建完成的消息事件(异步消息)、MQ消费者收到消息之后、会通知各处理器处理该消息、各处理器处理完后都会发布一条将该工单写入搜索引擎的消息、最终该工单出现在搜索引擎、被工单处理人检索和处理。 事故异常体现 1、异常体现 从工单的流转记录发现、

记一次难忘的json反序列化问题排查经历

前言 最近我在做知识星球中的商品秒杀系统,昨天遇到了一个诡异的json反序列化问题,感觉挺有意思的,现在拿出来跟大家一起分享一下,希望对你会有所帮助。 案发现场 我最近在做知识星球中的商品秒杀系统,写了一个filter,获取用户请求的header中获取JWT的token信息。 然后根据token信息

记一次 .NET某机械臂上位系统 卡死分析

一:背景 1. 讲故事 前些天有位朋友找到我,说他们的程序会偶发性的卡死一段时间,然后又好了,让我帮忙看下怎么回事?窗体类的程序解决起来相对来说比较简单,让朋友用procdump自动抓一个卡死时的dump,拿到dump之后,上 windbg 说话。 二:WinDbg 分析 1. 主线程在做什么 要想

记一次 .NET某工厂报警监控设置 崩溃分析

一:背景 1. 讲故事 前些天有位朋友在微信上丢了一个崩溃的dump给我,让我帮忙看下为什么出现了崩溃,在 Windows 的事件查看器上显示的是经典的 访问违例 ,即 c0000005 错误码,不管怎么说有dump就可以上windbg开干了。 二:WinDbg 分析 1. 程序为谁崩溃了 在 Wi