當使用者與伺服器之間的互動跨越多個請求時,如何儲存與管理這些請求之間的 session 狀態,是企業應用架構中的重要決策。本章介紹三種 session state 模式,各有不同的取捨。

Client Session State#

意圖#

session 狀態儲存在客戶端,讓伺服器保持無狀態。

運作方式#

客戶端在每次請求時發送完整的 session 資料,伺服器處理後將完整的 session 狀態隨回應送回。這讓伺服器可以完全無狀態(stateless)

通常使用 Data Transfer Object 來處理資料傳輸,因為它可以序列化並透過網路傳送複雜資料。

在 Rich Client 應用中,資料可以直接存放在客戶端的資料結構或 Data Transfer Object 中。在 HTML 介面中,有三種常見方式:

  • URL 參數:最簡單,但 URL 長度有限。適合少量資料,如 session ID。某些平台支援自動 URL 重寫來加入 session ID
  • Hidden Fields:透過 <INPUT type="hidden"> 標籤將資料嵌入頁面。需要將 session 狀態序列化為某種格式(如 XML 或文字編碼),但請注意查看頁面原始碼就能看到
  • Cookies:隨每次請求自動來回傳送。大小有限制,且使用者可能關閉 cookie 功能。cookie 僅在單一網域名稱下運作

客戶端資料容易被窺視和篡改。加密是唯一的防護手段,但會增加效能負擔。沒有加密的情況下,不要傳送任何敏感資料,且所有從客戶端回來的資料都必須完整重新驗證

使用時機#

  • 優點:完美支援無狀態伺服器,最大化叢集能力與容錯恢復能力
  • 缺點:隨著資料量增加,每次請求都傳輸所有狀態的效能成本會急劇上升
  • 安全風險:客戶端資料容易被竊取或竄改,需要加密或至少避免傳送敏感資訊
  • 幾乎所有應用都至少需要一點 Client Session State 來傳送 session ID——這通常只是一個數字,不會造成負擔
  • 需要注意 session 盜取的風險——惡意使用者可能更改 session ID 來劫持其他人的 session

Server Session State#

意圖#

session 狀態以序列化形式保存在伺服器端

運作方式#

最簡單的形式是將 session 物件保存在應用伺服器的記憶體中。透過一個以 session ID 為 key 的 map,客戶端只需提供 session ID,伺服器就能取出對應的 session 物件來處理請求。

這個基本方案假設:應用伺服器有足夠的記憶體、只有一台伺服器(無叢集)、以及伺服器當機時可以接受遺失 session。

記憶體資源問題#

常見的反對意見是 session 物件佔用記憶體資源。解決方法是將 session 狀態序列化為 memento([Gang of Four])做持久化儲存。序列化形式有兩種選擇:

  • 二進位序列化:程式設計較簡單,佔用空間較少,但不可人類閱讀,且類別結構變更後可能無法反序列化
  • 文字序列化(如 XML):需要較多程式碼,但可讀性較好

儲存位置#

序列化後的 session 可以存放在:

  • 應用伺服器本機(檔案系統或本地資料庫):簡單,但不支援叢集和容錯
  • 共享伺服器或資料庫:支援叢集和容錯,但存取較慢(快取可以緩解部分問題)

當你把序列化的 Server Session State 存入資料庫的 session 表格(使用 Serialized LOB)時,就來到了 Server Session State 和 Database Session State 的模糊邊界。作者以「是否將資料轉為表格形式」作為分界線。

過期 Session 的清理#

需要有機制清理過期的 session。一種有效的做法是將 session 表分成多個時間段(segments),每隔固定時間輪替——刪除最舊的段並將新寫入導向新段,超過一定時間的 session 即自動清除。

Java 實作#

Java 中兩種常見技術:

  • HTTP Session:簡單,由 Web 伺服器儲存。大多數情況下會導致伺服器親和性(server affinity),不支援容錯。部分廠商實作了共享 HTTP session(儲存在資料庫中)
  • Stateful Session Bean:由 EJB 容器處理所有持久化和被動化(passivation)。程式設計簡單,但規範不要求避免伺服器親和性(部分廠商如 IBM WebSphere 可以將 stateful bean 序列化到 DB2 的 BLOB 中)

很多人說 stateless session bean 效能總是優於 stateful session bean,但這是過度簡化的說法。應該先做負載測試,確認在你的負載量下兩者的效能差異是否真的顯著。如果 stateful bean 更容易開發且效能差異不大,那就用它。

使用時機#

  • 最大優點簡單——很多情況下幾乎不需要額外程式設計
  • 將 BLOB 序列化到資料庫表,往往比將 server 物件轉換為表格形式省事
  • 需要自行支援叢集和容錯時,程式設計工作量會增加。如果你的應用伺服器平台已經提供這些能力,那就更適合使用 Server Session State
  • 如果 session 資料不多或資料容易轉為表格形式,Database Session State 可能更合適

Database Session State#

意圖#

session 資料以已提交的資料形式儲存在資料庫中

運作方式#

每次客戶端請求到達伺服器時,伺服器物件從資料庫中提取該請求所需的資料,處理完後將所有需要的資料存回資料庫。客戶端只需在本地儲存一個 session ID(以及找到相關資料的 key)。

關鍵問題在於 session 資料通常是局部於該 session 的,不應在 session 整體確認前影響系統其他部分。例如,正在編輯中的訂單不應出現在庫存查詢或每日營收報表中。

區分 session 資料與記錄資料#

有兩種主要方式:

  • isPending 欄位:在每筆記錄上加一個 Boolean 欄位或 session ID 欄位。使用 session ID 更好,因為更容易找到特定 session 的所有資料。但這種方式侵入性很高——所有存取記錄資料的應用都需要知道如何過濾 session 資料
  • Pending Tables:為每個記錄表建立對應的 pending 表(結構完全相同,但加上 session ID 欄位)。這降低了侵入性,但需要在 mapping 程式碼中加入表格選擇邏輯

Pending tables 通常可以放寬完整性規則和驗證規則,因為 session 進行中的資料可能尚未完整。不同的驗證規則可能根據 session 的進度而有所不同。

清理機制#

需要機制清理被取消或遺棄的 session 資料。透過 session ID 可以找到並刪除所有相關資料。對於使用者無聲放棄的 session,需要某種逾時機制——例如一個 daemon 定期掃描過期的 session 資料。

回滾的複雜性#

如果 session 允許修改已有的記錄,回滾會變得非常複雜。一種簡單的做法是不允許取消整個 session——對已有記錄的修改在每次請求結束時就成為正式資料。另一種做法是將可能被修改的資料複製到 pending tables 中修改,在 session 結束時再寫回正式表,但這會變得很複雜。

最簡單的方式是完全不使用 pending 資料——將系統設計為所有資料都是記錄資料。雖然不總是可行,但如果能做到,Database Session State 就會容易很多。

使用時機#

  • 效能考量:可以使用無狀態的伺服器物件,支援 pooling 和簡易叢集。但代價是每次請求都需要從資料庫存取資料(快取可以減少讀取成本,但寫入成本仍在)
  • 程式設計工作量:如果沒有 session 狀態、所有資料都能以記錄資料方式處理,那這個模式非常自然。否則需要處理 pending 資料的複雜性
  • 與 Server Session State 比較,最大的差異可能在於你的應用伺服器支援叢集和容錯的難易度。Database Session State 的叢集和容錯通常更直接