ThreadLocal引起的一次線上事故
> 線上用戶存儲數據後查看提示無權限
前言
-
不知道什麼時候年輕的我曾一度認為Java沒啥難度,沒有我實現不了的需求,沒有我解不了的bug
-
直到我遇到至今難忘的一個bug 。 線上用戶存儲數據後查看提示無權限
初次定位
- 明明自己添加的數據,為什麼提示自己沒有權限呢?我一開始自信的認為是我們的客戶操作有問題、或者是我們權限配置有問題
- 但是帶我自己親自驗證了一下之後發現這個問題時現時不現,屬於一個偶發的問題。這個在開發階段還真的不容易發現。
問題升級
-
經過自己的測試後讓我更加懷疑人生了,你要麼就有問題要麼就沒問題。一會有一會沒有到底又是幾個意思呢?偶先的問題真的很難解決啊。問題定位到這裡我已經精疲力竭了。然後就放棄了定位
-
但是問題還是得解決,第二天我又硬着頭皮開始研究了。可能第二天頭腦比較清醒我發現我們系統中在插入數據的時候會自動獲取到當前登錄用戶並在數據庫中記錄次數據的創建者及最新的修改者。這更應該說明我們的問題離譜 。但是問題在我們獲取當前登錄用戶的時候出現了問題
-
對,我將問題追蹤了一下,終於將問題本質找到了。我們獲取當前登錄用戶是通過
ThreadLocal
來實現的。那麼問題就是“ThreadLocal` 獲取用戶有問題 -
我們分佈式開發系統。我們會在每個模塊里添加一個aop攔截器,通過請求頭的token再去user模塊查詢用戶基本信息。然後放到“ThreadLocal
中。這樣我們的系統中隨處都可以通過
ThreadLocal` 這個對象獲取我們的登陸用戶。 -
別問我為什麼要在每個模塊都這樣做?別問我為什麼用
ThreadLocal
?別問我為什麼是分佈式還要這樣做? 因為今天我們重點是解決bug
開門見山
- 問題就出現在getUser那塊邏輯里。因為我們的設計就是在系統中隨處都可以獲取到User對象。當然我們這裡指的是任何請求里。對於MQ、定時器這些模塊里肯定是沒有User的。因為這些沒法走AOP攔截
ThreadLocal獲取用戶信息亂串,導致用戶新增數據權限異常
最終定位
- 我們的
ThreadLocal
是個對象,我們系統中是通過一個工具類獲取這個對象的屬性的。在這個對象我們提供set、get方法。
- 上面的流程展示了在獲取到User用戶之後就會加入到工廠。如果工廠已經存在了就不會加入。否則就會加入我們的用戶
- 這樣也是避免我們不斷加入重複用戶信息。因為同一個線程對應的只可能是一個用戶。
思考
public static UserInfo getUser() {
return userThreadLocal.get();
}
- 上面是我們工具類的get方法。這就是將
ThreadLocal
對象存儲的內容返回出去。這一步應該不會出現問題。 - 在getUser中很明顯沒有問題,我們利用排除法只剩下了setUser了。雖然排除了別人的嫌疑但是setUser我還是看不出有什麼問題。經過一陣debug斷點跟蹤後我發現我們setUser邏輯的確有問題
- setUser是將用戶信息保存到“Threadlocal
對象中,但是前提是
ThreadLocal`中沒有用戶。對就是這個問題,如果已經有了用戶呢?那麼我們真正的用戶就會無法添加進去 - 到了這裡問題逐漸的明朗起來。使我們
ThreadLocal
對象管理的有問題。導致保存了上次的用戶信息從而導致用戶信息亂串的現象
解決問題
- 既然我們已經定位到
ThreadLocal
的管理問題,那麼我們就好辦了。
ThreadLocal簡單梳理
ThreadLocal
將對象保存在線程中。換句話說就是每個線程的數據會相互隔離。基於這個特性我們可以將用戶信息存儲在這裡,這樣我們能保證我們的當前線程下執行分各種方法都能通過他獲取到用戶信息ThreadLocal
內部是將已自己為key, 存儲對象為value存儲到當前線程中的map中。這個map會隨着線程的銷毀而被JVM回收。- 但是在我們實際開發中經常會使用線程池來避免線程的重複創建及銷毀。那麼線程往往是不會被銷毀的
- 在Spring中集成的類似Tomcat、JBoss等web容器中都是默認使用的一定數量的線程數的。而我們在spring中使用的線程復用功能就導致了我們在獲取當前線程的用戶時因為此線程被別人使用過從未導致用戶信息沒有被更新成功。從而引發我們上面提到的奇怪的問題
- 那麼既然是沒有被更新,到這裡我們就很好解決了,要麼每次使用完成後都將
ThreadLocal
中的數據remove。因為他內部是弱引用在下次回收就會將對象回收這樣也不會造成內存泄漏的問題 - 或者我們在我們的AOP中setUser之前先將用戶
ThreadLocal
清空。兩種方式都可以完美解決我們的問題
具體代碼實現
/**
* 請求生命周期最後一步銷毀是做的回調事件
* 用於銷毀在線用戶信息,防止在線用戶信息互相干擾(在多線程復用時)
*/
@WebListener
@Primary
public class SysServletRequestListener implements ServletRequestListener {
@Override
public void requestDestroyed(ServletRequestEvent requestEvent) {
UserInfoUtil.clearUserInfo();
}
@Override
public void requestInitialized(ServletRequestEvent sre) {
}
}
- 我們可以通過spring提供的監聽器,監聽一個請求的生命周期在這個請求完成之後將我們的
ThreadLocal
進行remove。 為什麼我推薦這種做法呢。因為請求結束就清空可以快速的讓出內存讓他去做更加有用的事情。 - 如果是第二種方法那麼如果我們沒有人在登錄,或者說在下一次登錄之前這塊不需要的內存永遠被佔著
總結忠告
- 這次問題出現的很是奇怪,一度讓我懷疑人生,但是永遠相信程序是不會無緣無故的出問題的。
- 出問題的只能是我們的代碼有問題,要善於解析問題,將問題細化,細化到我們代碼層面而不是業務層面
- 使用一個技術時最好能先了解他內部的一個原理。或者最起碼先了解他的大概邏輯
- 別看這篇文章寥寥幾字就解決了我們的問題,但是實際上我在解決他的過程中吃了不少的苦。好幾個夜晚都是我在陪他戰鬥
- 我在定位到時
ThreadLocal
後就花了一個小時學習了下他的邏輯並跟蹤了他的源碼。最後結合我們的業務才發現了眉目 - 總之有問題是好事情,有了問題我們才能成長。至少在這次的問題中我學習到了
ThreadLocal
。我的這次問題也是使用他的典型問題,另外還有一個內存泄漏的問題這是在學習他源碼的過程領悟到的一點。關於內存泄漏我們有時間在看吧。問題解決。終於可以繼續happy了。