前言
記錄一次開發中遇到的關於 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 中的資料。