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()


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM