zoukankan      html  css  js  c++  java
  • ThreadLocal操作不当引起的bug

    背景

    项目是简单的web项目,多用户登陆的商家管理系统,使用ThreadLocal缓存登陆用户的信息(duid,用户唯一id)

    bug描述

    在测试环境多次登陆后,调用查询接口查出的数据时有时无

    排查过程

    通过商户id和用户的duid给日志打上唯一标识(测试环境日志太多了),以便grep,排查后发现数据和日志还是时有时无,在排查中发现duid有时对有时错,于是duid便成了突破口。顺藤摸瓜,找到了拦截器缓存的duid数据,然而发现拦截器缓存的没有问题。对比别的项目的拦截器后发现了问题,拦截器有个方法没有重写且本地线程的数据也没有remove

        @Override
        public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
            DataUserHolder.clear();
            super.afterCompletion(request, response, handler, ex);
        }
    

    这个加上了,bug就解决了。

    思考

    为什么threadlocal的数据会错乱(被覆盖)?

    画了一张简图来表示ThreadLocal的内部结构。

    ThreadLocal内部实际使用了ThreadLocalMap来缓存数据。

    一个entry即一个对象,可以理解为一个键值对。

    ThreadLocalMap内部使用Entry[]来存储对象。

    到目前为止,我们尚未分析源码,但并不妨碍我们根据结果以及加粗文字推导问题原因。

    如果我们简单的把ThreadLocalMap理解为HashMap,是不是问题就显而易见了?

    以当前线程为key,以登陆用户数据为value,在线程不变的情况下,用户数据变了,有没有这个可能?

    有可能。

    此处应有理论(个人):服务端只认请求线程,不认请求数据

    为什么这么说呢?

    比如在同一个浏览器上前后登陆两个账号,最后一定登陆的是后面的账号,服务器认的是请求线程而不是账号密码。

    代码模拟bug过程

    public class TestMain {
        @Test
        public void test() {
    
            final ThreadLocal<UserCacheVO> local = new ThreadLocal<>();
            final UserCacheVO vo1 = new UserCacheVO();
            vo1.setDuid("12345");
            vo1.setPhone("123434324123");
            local.set(vo1);
            UserCacheVO vo2 = new UserCacheVO();
            vo2.setDuid("xxxx");
            vo2.setPhone("yyygyjbjh");
            local.set(vo2);
            System.out.println(local.get());
        }
    }
    
    UserCacheVO(phone=yyygyjbjh, duid=xxxx, userInfoMap=null)
    Process finished with exit code 0
    

    代码流程:本来的业务需求是使用vo1的数据去db查询结果,结果vo1的数据能正常查到结果,此时我用vo2的数据再次去查询,就查不到了(数据已覆盖)

    对应页面流程:页面登录,拦截器缓存数据,查询结果,正常页面展示;换账号登录后,拦截器缓存数据,覆盖之前的请求线程的数据,导致数据的duid覆盖,此时查询的结果已不是我们想要的业务结果,在服务器里使用 merchantId+duid查询数据就会发现没这个日志,就出现莫名其妙的bug了。

    修改bug后的代码流程:页面登录,拦截器缓存数据,查询结果,拦截器remove缓存,正常页面展示。

    注:登陆这个模块是单独的服务,且登陆服务由前端直接调用,正确登陆前端则获取ticket做业务调用

    源码分析

    private void set(ThreadLocal<?> key, Object value) {
    
        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.
    
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
    
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    				// 重点
            if (k == key) {
                e.value = value;
                return;
            }
    
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    

    如果ThreadLocal 相同,则Entry直接覆盖。

    总结

    org.springframework.web.servlet.handler.HandlerInterceptorAdapter共有四个方法,分别是

    preHandle

    进入controller接口前执行

    postHandle

    在 DispatcherServlet 呈现视图(ModelAndView)之前调用,在前后端分离后好像就没有视图一说了,不甚了解

    afterCompletion

    请求处理完成后的回调,即渲染视图后。执行完controller接口后执行,可以做资源清理。

    afterConcurrentHandlingStarted

    并发执行时调用,一般用不到

    此bug重点在于本地线程的数据用完后没有清理,即未调用afterCompletionDataUserHolder.clear()

    三分热血值得你十二分努力。
  • 相关阅读:
    Building Java Projects with Gradle
    Vert.x简介
    Spring及Spring Boot 国内快速开发框架
    dip vs di vs ioc
    Tools (StExBar vs Cmder)which can switch to command line window on context menu in windows OS
    SSO的定义、原理、组件及应用
    ModSecurity is an open source, cross-platform web application firewall (WAF) module.
    TDD中测试替身学习总结
    Spring事务银行转账示例
    台式机(华硕主板)前面板音频接口(耳机和麦克风)均无声的解决办法
  • 原文地址:https://www.cnblogs.com/woooodlin/p/threadlocal_data_not_remove.html
Copyright © 2011-2022 走看看