文章目錄
- 雙攔截器
- ThreadLocal實現原理
雙攔截器
實現登錄狀態刷新的原因:
? 防止用戶會話過期:通過動態刷新Token有效期,確保活躍用戶不會因固定過期時間而被強制登出
? 提升用戶體驗:用戶無需頻繁重新登錄,只要在活動期間就能保持登錄狀態
在登錄狀態校驗時,只設置了一個攔截器:
只設置一個攔截器時:攔截器只攔截刷新需要登錄的路徑,如果用戶在登錄后長時間不訪問任何需要攔截的路徑,那么他們的登錄令牌可能不會得到及時刷新,導致令牌過期。一旦用戶嘗試訪問需要攔截的路徑,他們可能會發現自己需要重新登錄,因為令牌已經失效。所以單獨創建一個攔截器攔截一切請求,刷新Redis中的Key
雙攔截器執行流程:
那么我們可以添加一個攔截器,第一個攔截器攔截所有路徑,首先從請求頭中獲取token,如果token存在,則通過token查詢redis判斷判斷該token是否存在redis中(有可能是別的網站的token,所以要判斷是否在redis中)和有沒有過期(過期了會采用過期淘汰策略…有可能直接查不到/查到過期了就直接刪除),如果token存在reids中且沒有過期,將用戶信息(UserDTO對象,包括用戶id、昵稱等)保存到theadlocal然后刷新token有效期并放行,如果token不存在或過期了則不執行任何操作并放行。
第一個攔截器只進行刷新token操作不攔截,第二個攔截器攔截需要登錄的路徑,判斷ThreadLocal中的是否存在用戶信息,如果用戶存在就說明登錄了就放行,否則執行攔截操作。(只創建了一個threadlocal線程對象,threadlocalmap中只存了一個對象,直接判斷是否為空就可以)
token來源:注冊登錄后,后端會生成一個token作為用戶的唯一id,將這個token作為key用戶信息作為value存入redis中(還設置有效期),同時將這個token返回給前端,前端的每次請求都會攜帶這個token進行登錄狀態校驗操作。
Threadlocal中存入的用戶信息的作用:
- 在第二個攔截器中可以用來判斷是否存在用戶信息,進而完成用戶登錄攔截操作
- 在后面一人一單判斷過程中,需要從Threadlocal中取出用戶id來構造用戶級細粒度鎖。
未登錄攔截的實現:如果需要登錄攔截,則返回HTTP狀態碼為401
(未授權),前端根據狀態碼跳轉到登錄頁。
攔截器使用:1.定義攔截器 2.注冊配置攔截器
雙攔截器通過設置優先級來實現執行的先后順序(第一個攔截器優先級高order=0,第二個攔截器order=1)
ThreadLocal實現原理
客戶端每一次發起的請求都是單獨的一個線程,所以可以用ThreadLocal
ThreadLocal 是 Java 中的一個工具類,通過threadlocal類可以創建線程對象,threadlocal會在每個線程內開辟一個內存空間去保存每個線程的數據,可以實現線程間的數據隔離,避免線程安全問題。
ThreadLocal是用于解決線程安全的一種機制,它允許創建線程局部變量,每個線程自己獨立的變量副本,從而避免了線程之間的資源共享和同步問題。
這里說的副本指的是每個線程擁有該變量的獨立實例,線程之間不會共享相同的變量,從而實現了線程隔離和數據安全。
ThreadLocal 的作用
- 線程隔離: 每個線程擁有自己的變量副本,互不干擾。
- 避免共享: 無需使用鎖或同步機制,提升并發性能。
- 簡化設計: 方便在多線程環境中傳遞上下文信息(如用戶會話、事務 ID)。
ThreadLocal 的實現原理
主要是通過Thread類中的ThreadLocalMap字段來實現的。
- ThreadLocalMap: 每個線程內部都有自己的
ThreadLocalMap
,用于存儲ThreadLocal
變量,一個線程可以創建多個ThreadLocal線程對象,如ThreadLocal1、ThreadLocal2等,存在ThreadLocalMap中的不同位置。 - 鍵值對存儲:
ThreadLocal
對象本身作為鍵,變量副本作為值。
ThreadLocal 的常用方法
- public void set(T value) 設置當前線程的線程局部變量的值
- public T get() 返回當前線程所對應的線程局部變量的值
- public void remove() 移除當前線程的線程局部變量
使用場景
- 線程上下文傳遞: 如用戶會話、事務 ID。
- 數據庫連接管理: 每個線程使用獨立的數據庫連接。
- 日期格式化:
SimpleDateFormat
非線程安全,可使用ThreadLocal
為每個線程創建獨立實例。
ThreadLocalMap 只由數組組成,通過開放地址法中的線性探測(線性向后查找)的方式解決hash沖突。具體的:如果 i 位置被占用,嘗試 i+1。如果 i+1 也被占用,繼續探測 i+2,直到找到一個空位。如果到達數組末尾,則回到數組頭部,繼續尋找空位。
為什么用線性探測法而不用hashmap的拉鏈法?因為ThreadLocalMap 不會有大量的 Key,所以采用線性探測更節省空間。
GC 之后 key 是否為 null? 是null,因為key是弱引用,gc回收后,key為null,但是value是強引用,垃圾回收后還會存在。
用完之后要及時執行remove方法
ThreadLocalMap 擴容機制:
采用的是“先清理再擴容”的策略,元素個數達到閾值(0.75*總容量)時,會先清理掉被垃圾回收掉key的entry對象,然后再檢查size是否到閾值,擴容時,數組長度翻倍,并重新計算索引,如果發生哈希沖突,采用線性探測法來解決。
使用 InheritableThreadLocal
時,會在創建子線程時,令子線程繼承父線程中的 ThreadLocal
值,但是無法支持線程池場景下的 ThreadLocal
值傳遞。
還有TransmittableThreadLocal:TransimittableTreadLocal 是 TreadLocal 的增強。它與InheritableThreadLocal 相比,更適合在線程池中父線程與子線程傳遞的場景。ITL 只是在子線程被創建時繼承一次父線程的值,之后如果子線程自己修改了值,就會一直復用這個值,不會拉取父線程的值,并且也感知不到父線程值得變化。而 TTL,是任務級別的動態捕獲,每次任務提交時,會動態捕獲父線程的最新值。通過捕獲上下文、傳遞上下文、恢復上下文的方式完成。 鏈接
每個線程都維護一個ThreadLocalMap,一個線程可以創建多個線程對象
public class MultipleThreadLocalsDemo {// 定義多個ThreadLocal變量private static final ThreadLocal<String> userContext = new ThreadLocal<>();private static final ThreadLocal<Integer> requestId = new ThreadLocal<>();private static final ThreadLocal<SimpleDateFormat> dateFormat = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));public static void main(String[] args) {// 主線程設置多個ThreadLocal值userContext.set("用戶A");requestId.set(1001);// 獲取值(互不干擾)System.out.println(userContext.get()); // 輸出"用戶A"System.out.println(requestId.get()); // 輸出1001System.out.println(dateFormat.get().format(new Date())); // 輸出當前日期// 必須顯式清理(防止內存泄漏)userContext.remove();requestId.remove();dateFormat.remove();}
}