1. 程式人生 > >從Spring-Session原始碼看Session機制的實現細節

從Spring-Session原始碼看Session機制的實現細節

作者:徐靖峰

原文:https://www.cnkirito.moe/2018/04/17/spring-session-4/

640?wx_fmt=png&wxfrom=5&wx_lazy=1

去年本文作者曾經寫過幾篇和 Spring Session 相關的文章,從一個未接觸過 Spring Session 的初學者視角介紹了 Spring Session 如何上手,如果你未接觸過 Spring Session,推薦先閱讀下「從零開始學習Spring Session」系列:

Spring Session 主要解決了分散式場景下 Session 的共享問題,本文將從 Spring Session 的原始碼出發,來討論一些 Session 設計的細節。

Spring Session 資料結構解讀

想象一個場景,現在一到面試題呈現在你面前,讓你從零開始設計一個 Session 儲存方案,你會怎麼回答?

說白了就是讓你設計一套資料結構儲存 Session,並且我相信提出這個問題時,大多數讀者腦海中會浮現出 redis,設計一個 map,使用 ttl 等等,但沒想到的細節可能會更多。先來預覽一下 Spring Session 的實際資料結構是什麼樣的(使用 spring-session-redis 實現),當我們訪問一次集成了Spring Session 的 web 應用時

  1. @RequestMapping("/helloworld")

  2. publicString hello(HttpSession session

    ){

  3.  session.setAttribute("name","xu");

  4. return"hello.html";

  5. }

可以在 Redis 中看到如下的資料結構:

  1. A)"spring:session:sessions:39feb101-87d4-42c7-ab53-ac6fe0d91925"

  2. B)"spring:session:expirations:1523934840000"

  3. C)"spring:session:sessions:expires:39feb101-87d4-42c7-ab53-ac6fe0d91925"

這三種鍵職責的分析將會貫徹全文,為了統一敘述,在此將他們進行編號,後續簡稱為 A 型別鍵,B 型別鍵,C 型別鍵。先簡單分析下他們的特點

  • 他們公用的字首是 spring:session

  • A 型別鍵的組成是字首 +”sessions”+sessionId,對應的值是一個 hash 資料結構。在我的 demo 中,其值如下

  1. {

  2. "lastAccessedTime":1523933008926,/*2018/4/1710:43:28*/

  3. "creationTime":1523933008926,/*2018/4/1710:43:28*/

  4. "maxInactiveInterval":1800,

  5. "sessionAttr:name":"xu"

  6. }

其中 creationTime(建立時間),lastAccessedTime(最後訪問時間),maxInactiveInterval(session 失效的間隔時長) 等欄位是系統欄位,sessionAttr:xx 可能會存在多個鍵值對,使用者存放在 session 中的資料如數存放於此。

A 型別鍵對應的預設 TTL 是 35 分鐘。

  • B 型別鍵的組成是字首+”expirations”+時間戳,無需糾結這個時間戳的含義,先賣個關子。其對應的值是一個 set 資料結構,這個 set 資料結構中儲存著一系列的 C 型別鍵。在我的 demo 中,其值如下

  1. [

  2. "expires:39feb101-87d4-42c7-ab53-ac6fe0d91925"

  3. ]

B 型別鍵對應的預設 TTL 是 30 分鐘

  • C 型別鍵的組成是字首+”sessions:expires”+sessionId,對應一個空值,它僅僅是 sessionId 在 redis 中的一個引用,具體作用繼續賣關子。

C 型別鍵對應的預設 TTL 是 30 分鐘。

kirito-session 的天使輪方案

介紹完 Spring Session 的資料結構,我們先放到一邊,來看看如果我們自己設計一個 Session 方案,擬定為 kirito-session 吧,該如何設計。

kirito 的心路歷程是這樣的:“使用 redis 存 session 資料,對,session 需要有過期機制,redis 的鍵可以自動過期,肯定很方便。”

於是 kirito 設計出了 spring-session 中的 A 型別鍵,複用它的資料結構:

  1. {

  2. "lastAccessedTime":1523933008926,

  3. "creationTime":1523933008926,

  4. "maxInactiveInterval":1800,

  5.    key/value...

  6. }

然後對 A 型別的鍵設定 ttl A 30 分鐘,這樣 30分鐘之後 session 過期,0-30 分鐘期間如果使用者持續操作,那就根據 sessionId 找到 A 型別的 key,重新整理 lastAccessedTime 的值,並重新設定 ttl,這樣就完成了「續簽」的特性。

顯然 Spring Session 沒有采用如此簡練的設計,為什麼呢?翻看 Spring Session 的文件

One problem with relying on Redis expiration exclusively is that Redis makes no guarantee of when the expired event will be fired if the key has not been accessed. Specifically the background task that Redis uses to clean up expired keys is a low priority task and may not trigger the key expiration. For additional details see Timing of expired events section in the Redis documentation.

大致意思是說,redis 的鍵過期機制不“保險”,這和 redis 的設計有關,不在此拓展開,研究這個的時候翻了不少資料,得出瞭如下的總結:

  1. redis 在鍵實際過期之後不一定會被刪除,可能會繼續存留,但具體存留的時間我沒有做過研究,可能是 1~2 分鐘,可能會更久。

  2. 具有過期時間的 key 有兩種方式來保證過期,一是這個鍵在過期的時候被訪問了,二是後臺執行一個定時任務自己刪除過期的 key。劃重點:這啟發我們在 key 到期後只需要訪問一下 key 就可以確保 redis 刪除該過期鍵

  3. 如果沒有指令持續關注 key,並且 redis 中存在許多與 TTL 關聯的 key,則 key 真正被刪除的時間將會有顯著的延遲!顯著的延遲!顯著的延遲!

天使輪計劃慘遭破產,看來單純依賴於 redis 的過期時間是不可靠的,秉持著力求嚴謹的態度,迎來了 A 輪改造。

A 輪改造—引入 B 型別鍵確保 session 的過期機制

redis 的官方文件啟發我們,可以啟用一個後臺定時任務,定時去刪除那些過期的鍵,配合上 redis 的自動過期,這樣可以雙重保險。第一個問題來了,我們將這些過期鍵存在哪兒呢?不找個合適的地方存起來,定時任務到哪兒去刪除這些應該過期的鍵呢?總不能掃描全庫吧!來解釋我前面賣的第一個關子,看看 B 型別鍵的特點:

  1. spring:session:expirations:1523934840000

時間戳的含義

1523934840000 這明顯是個 Unix 時間戳,它的含義是存放著這一分鐘內應該過期的鍵,所以它是一個 set 資料結構。解釋下這個時間戳是怎麼計算出來的org.springframework.session.data.redis.RedisSessionExpirationPolicy#roundUpToNextMinute

  1. staticlong roundUpToNextMinute(long timeInMs){

  2. Calendar date =Calendar.getInstance();

  3.        date.setTimeInMillis(timeInMs);

  4.        date.add(Calendar.MINUTE,1);

  5.        date.clear(Calendar.SECOND);

  6.        date.clear(Calendar.MILLISECOND);

  7. return date.getTimeInMillis();

  8. }

還記得 lastAccessedTime=1523933008926,maxInactiveInterval=1800 吧,lastAccessedTime 轉換成北京時間是: 2018/4/1710:43:28,向上取整是 2018/4/1710:44:00,再次轉換為 Unix 時間戳得到 1523932980000,單位是 ms,1800 是過期時間的間隔,單位是 s,二者相加 1523932980000+1800*1000=1523934840000。這樣 B 型別鍵便作為了一個「桶」,存放著這一分鐘應當過期的 session 的 key。

後臺定時任務

org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanupExpiredSessions

  1. @Scheduled(cron ="${spring.session.cleanup.cron.expression:0 * * * * *}")

  2. publicvoid cleanupExpiredSessions(){

  3. this.expirationPolicy.cleanExpiredSessions();

  4. }

後臺提供了定時任務去“刪除”過期的 key,來補償 redis 到期未刪除的 key。方案再描述下,方便大家理解:取得當前時間的時間戳作為 key,去 redis 中定位到 spring:session:expirations:{當前時間戳} ,這個 set 裡面存放的便是所有過期的 key 了。

續簽的影響

每次 session 的續簽,需要將舊桶中的資料移除,放到新桶中。驗證這一點很容易。

在第一分鐘訪問一次 http://localhost:8080/helloworld 端點,得到的 B 型別鍵為:spring:session:expirations:1523934840000;第二分鐘再訪問一次 http://localhost:8080/helloworld 端點,A 型別鍵的 lastAccessedTime 得到更新,並且 spring:session:expirations:1523934840000 這個桶被刪除了,新增了 spring:session:expirations:1523934900000 這個桶。當眾多使用者活躍時,桶的增刪和以及 set 中資料的增刪都是很頻繁的。對了,沒提到的一點,對應 key 的 ttl 時間也會被更新。

kirito-session 方案貌似比之前嚴謹了,目前為止使用了 A 型別鍵和 B 型別鍵解決了 session 儲存和 redis 鍵到期不刪除的兩個問題,但還是存在問題的。

B 輪改造—優雅地解決 B 型別鍵的併發問題

引入 B 型別鍵看似解決了問題,卻也引入了一個新的問題:併發問題。

來看看一個場景:

假設存在一個 sessionId=1 的會話,初始時間戳為 1420656360000

  1. spring:session:expirations:1420656360000->[1]

  2. spring:session:session:1-><session>

接下來迎來了併發訪問,(使用者可能在瀏覽器中多次點選):

  • 執行緒 1 在第 2 分鐘請求,產生了續簽,session:1 應當從 1420656360000 這個桶移動到 142065642000 這個桶

  • 執行緒 2 在第 3 分鐘請求,也產生了續簽,session:1 本應當從 1420656360000 這個桶移動到 142065648000 這個桶

  • 如果上兩步按照次序執行,自然不會有問題。但第 3 分鐘的請求可能已經執行完畢了,第 2 分鐘才剛開始執行。

像下面這樣:

執行緒 2 從第一分鐘的桶中移除 session:1,並移動到第三分鐘的桶中

  1. spring:session:expirations:1420656360000->[]

  2. spring:session:session:1-><session>

  3. spring:session:expirations:1420656480000->[1]

執行緒 1 完成相同的操作,它也是基於第一分鐘來做的,但會移動到第二分鐘的桶中

  1. spring:session:expirations:1420656360000->[]

  2. spring:session:session:1-><session>

  3. spring:session:expirations:1420656420000->[1]

最後 redis 中鍵的情況變成了這樣:

  1. spring:session:expirations:1420656360000->[]

  2. spring:session:session:1-><session>

  3. spring:session:expirations:1420656480000->[1]

  4. spring:session:expirations:1420656420000->[1]

後臺定時任務會在第 32 分鐘掃描到 spring:session:expirations:1420656420000 桶中存在的 session,這意味著,本應該在第 33 分鐘才會過期的 key,在第 32 分鐘就會被刪除!

一種簡單的方法是使用者的每次 session 續期加上分散式鎖,這顯然不能被接受。來看看 Spring Session 是怎麼巧妙地應對這個併發問題的。

org.springframework.session.data.redis.RedisSessionExpirationPolicy#cleanExpiredSessions

  1. publicvoid cleanExpiredSessions(){

  2. long now =System.currentTimeMillis();

  3. long prevMin = roundDownMinute(now

    相關推薦

    Spring-Session原始碼Session機制實現細節

    作者:徐靖峰原文:https://www.cnkirito.moe/2018/04/17/spr

    koa-session原始碼解讀session本質

    前言 Session,又稱為“會話控制”,儲存特定使用者會話所需的屬性及配置資訊。存於伺服器,在整個使用者會話中一直存在。 然而: session 到底是什麼? session 是存在伺服器記憶體裡,還是web伺服器原生支援? http請求是無狀態的,為什麼每次伺服器能取到你的

    Vue.js原始碼非同步更新DOM策略及nextTick

    寫在前面 因為對Vue.js很感興趣,而且平時工作的技術棧也是Vue.js,這幾個月花了些時間研究學習了一下Vue.js原始碼,並做了總結與輸出。 文章的原地址:https://github.com/answershuto/learnVue。 在學習過程中,為Vue加上了中文的註釋https:/

    wait的原始碼撤銷偏向鎖的過程(revoke and rebias)

    wait原始碼實現如下 //TRAPS表示是否有異常 void ObjectSynchronizer::wait(Handle obj, jlong millis, TRAPS) { if (UseBiasedLocking) { //如果是使用了偏向鎖,要撤銷偏向鎖

    手寫SpringMVC實戰,Spring底層原始碼分析與設計

    課程內容: 1,三分鐘熟悉Spring底層原始碼,你只需準備好鮮花即可; 2,Spring原始碼很可怕?那是因為你沒聽過James的課; 3,快速熟悉原始碼基礎,洞析SpringMVC與Spring框架關係; 4,@Controller,@Service這些註解算什麼,一

    Spring框架原始碼解析 IOC容器實現BeanDefinition(三)

    我們找女朋友,首先必須保證是個女的,這是最低要求,生活不易,我們先從最低的要求出發吧。女朋友是一個抽象的概念,我們必須定義一些屬性,年齡,身高,名字,是否漂亮等等來描述她。不過目前這些統統都沒有,有的也就是說我們的最低要求,女的。 public interface Bean

    0CTF一道題move_uploaded_file的一個細節問題

    前段時間的0CTF中有道題,其中涉及到了檔案上傳,並用到了move_uploaded_file()函式,但是有一個小問題不太明白,之後又繼續分析了一段時間,這裡給出關鍵程式碼:case 'upload': if (!isset($_GET["name"]) || !i

    Spring Security框架下JWT的實現細節原理

    一、回顧JWT的授權及鑑權流程 在筆者的上一篇文章中,已經為大家介紹了JWT以及其結構及使用方法。其授權與鑑權流程濃縮為以下兩句話 授權:使用可信使用者資訊(使用者名稱密碼、簡訊登入)換取帶有簽名的JWT令牌 鑑權:解籤JWT令牌,校驗使用者許可權。具有某個介面訪問許可權,開放該介面訪問。 二、S

    Spring-Session源碼Session機制實現細節

    任務 policy 系統 輔助 過期事件 討論 是什麽 統一 ext Re:從零開始的Spring Session(一) Re:從零開始的Spring Session(二) Re:從零開始的Spring Session(三) 去年我曾經寫過幾篇和 Sp

    原始碼Android】03Android MessageQueue訊息迴圈處理機制(epoll實現

    1 enqueueMessage handler傳送一條訊息 mHandler.sendEmptyMessage(1);經過層層呼叫,進入到sendMessageAtTime函式塊,最後呼叫到enqueueMessageHandler.java public bool

    PHP函式原始碼SESSION實現機制

    Session是以擴充套件的形式嵌入到PHP核心的,所以我們可以把Session當成擴充套件來看待。 一般擴充套件被載入到PHP會經過下面幾個過程 #define PHP_MINIT_FUNCTION      ZEND_MODULE_STARTU

    Spring Boot(十一)Redis整合Docker安裝到分散式Session共享

    一、簡介 Redis是一個開源的使用ANSI C語言編寫、支援網路、可基於記憶體亦可持久化的日誌型、Key-Value資料庫,並提供多種語言的API,Redis也是技術領域使用最為廣泛的儲存中介軟體,它是「Remote Dictionary Service」首字母縮寫,也就是「遠端字典服務」。 Red

    原始碼Spring Boot 2.0.1

    Spring Boot 命名配置很少,卻可以做到和其他配置複雜的框架相同的功能工作,從原始碼來看是怎麼做到的。 我這裡使用的Spring Boot版本是 2.0.1.RELEASE Spring Boot最重要的註解: @SpringBootApplication 開啟它: 其

    巧用session機制實現使用者不重複登入、記錄使用者登入日誌、統計線上人數

    HttpSessionBindingListener 這個具體的使用文件自查,本篇中是新建了一個類實現本介面 public class UsersOnlineCountListener implements HttpSessionBindingListe

    單點登入實現spring session+redis完成session共享

    v一、前言   專案中用到的SSO,使用開源框架cas做的。簡單的瞭解了一下cas,並學習了一下 ,有興趣的同學也可以學習一下,寫個demo玩一玩。 v二、工程結構      我模擬了 sso的客戶端和sso的服務端, sso-core中主要是一些sso需要的過濾器和工具類

    spring-session簡介、使用及實現原理

    一:spring-session 介紹 1.簡介 session一直都是我們做叢集時需要解決的一個難題,過去我們可以從serlvet容器上解決,比如開源servlet容器-tomcat提供的tomcat-redis-session-m

    原始碼Spring bean 生命週期

    在Spring中,bean一般都以單例模式存在,除非我們將singleton屬性設為false。 單例在多執行緒的環境下需要考慮執行緒安全的問題,對於一些公共的資源或資料應該怎麼處理才能保證安全,應該在什麼時機訪問這些資源最恰當。 熟悉了spring bean的整個生命週

    spring-session原始碼解讀-5

    session通用策略 Session在瀏覽器通常是通過cookie儲存的,cookie裡儲存了jessionid,代表使用者的session id。一個訪問路徑只有一個session cookie(事實上在客戶端就只有一個cookie,jsessionid是

    Spring Session 原始碼分析(1)——springSessionRepositoryFilter

    #Tomcat Session 對於session 是一個老生暢談的話題了,Session管理是JavaEE容器比較重要的一部分, Tomcat中主要由每個context容器內的一個Manager物件來管理session。對於這個manager物件的實現,可以

    【Android】原始碼角度Handler機制

    在Android開發規範中,規定了主執行緒的任務的響應時間不能超過5s,否則會出現ANR,即程式無響應。為了避免這個問題的出現,常用的一個解決方案就是開闢新執行緒,在開闢出來的子執行緒中去處理耗時的業務,然後回到UI執行緒(主執行緒)來重新整理UI,這個過程中“