🎯 什麼情境該想到我
當你「想用最簡單直接的方式保存一段使用者會話狀態」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把會話狀態存在伺服器端(記憶體或 session 物件),用一個 session id 對應。
做法要點:
- 狀態放伺服器記憶體,用戶端只持有 session id。
- 實作最簡單,多數應用伺服器內建 session 機制可直接用。
- 物件形式保存,程式存取自然、不需序列化到用戶端。
★★ 最重要的一個決定:狀態放「哪一台」伺服器
★ ① 行程內記憶體(最快,但★★ 只能單實例)
→ ★ 擴到兩個 pod 就壞了:使用者的請求輪到另一台 → session 不存在
→ ★ 補救:sticky session(★ 而它讓滾動部署、autoscaling、故障轉移全部變痛)
★★ ② 共享儲存(Redis/Memcached)—— ★ 實務標準
→ ★ 任何實例都能處理任何請求
→ ★★ 代價:① 多一個必須存活的依賴 ② 每個請求多一次網路往返
★ ③ 資料庫 → 見 [[資料庫會話狀態]]
★★
sticky session是最容易後悔的選擇:
它讓「把一台 pod 下線」變成「那些使用者被登出」,
而滾動部署就是一直在下線 pod。
★★ Redis 當 session store:五個實務細節
// ① ★★ 一定要設 TTL,而且「每次存取都續期」
if err := rdb.Set(ctx, key, data, 30*time.Minute).Err(); err != nil { … }
// ★ 沒有 TTL → Redis 記憶體無限成長 → ★★ 最後觸發 maxmemory 策略
// → 若策略是 noeviction,★ 寫入開始失敗(整個服務掛)
// → 若是 allkeys-lru,★★ 它會隨機淘汰「還有效的 session」⇒ 使用者被隨機登出
// ② ★★ Redis 掛了要怎麼辦(★ 這一題必須事先決定)
// a. ★ 全部視為未登入 → 所有使用者被登出,但服務還活著
// b. ★★ 回 503 → 誠實,但服務完全不可用
// → ★ 多數情況選 a,但★ 要確保「未登入」不會導致資料損壞
// ③ ★★ 序列化格式會變
// gob/json 存進去的 struct 加一個欄位 → ★ 舊 session 反序列化失敗
// → ★ 一律:① 用 json(可容忍未知欄位)② session 裡放 version
// ③ 反序列化失敗就當「未登入」,不要 500
// ④ ★★ 併發寫入同一個 session
// 兩個請求同時讀 session、各改一個欄位、各自寫回 → ★ 後寫的覆蓋前者
// → ★ 這就是 [[樂觀離線鎖]] 的問題,在 session 上一樣存在
// → ★★ 對策:① session 只放「不會併發改的東西」② 或用 Redis 的 WATCH/Lua
// ⑤ ★ session ID 必須是密碼學隨機
sid := make([]byte, 32); rand.Read(sid) // ★★ crypto/rand,不是 math/rand
// ★ 而且登入成功後要「換一個新的 session ID」(防 session fixation)★★ ①是實務上最常見的生產事故:沒設 TTL 或 maxmemory 策略錯,
結果是「使用者被隨機登出」或「Redis 寫入失敗導致服務掛」——
而兩者的症狀都不會指向 session 設定。
★★ session 裡該放什麼(越少越好)
✅ ★ user_id、session 建立時間、CSRF token、最小的權限旗標
❌ ★★ 整個使用者物件 —— ★ 使用者資料改了,session 裡還是舊的
❌ ★★ 購物車、多步驟表單 —— ★ 應該存資料庫(見 [[資料庫會話狀態]])
❌ ★ 權限清單 —— ★★ 改權限不會即時生效,而這常常是安全需求
★★ 判準:這個資料「過時了會不會出問題」?
會(權限、餘額、帳號狀態)→ 不要放 session,每次查。
不會(顯示語言、上次瀏覽的分類)→ 可以放。
★★ 它相對 JWT 的唯一真正優勢:可即時撤銷
★ 登出、封鎖帳號、改密碼踢掉其他裝置、變更權限
→ ★★ 全部只需要「刪掉/改掉那個 session」⇒ 下一個請求立即生效
→ ★ 這是 [[用戶端會話狀態]] 做不到的(見該頁的 session_version 折衷)
★ 另一個常被忽略的優勢:可觀測性。
「目前有多少人在線」「這個使用者有幾個裝置登入」——
★★ 有 session store 就查得到,JWT 查不到。
★ 什麼時候用它
- ★★ 需要即時撤銷(幾乎所有有使用者的系統都需要)。
- ★ 狀態稍大(幾 KB 的表單暫存)——不適合塞進 cookie。
- ★ 需要知道「誰在線」。
★ 什麼時候不用
- ★ 純機器對機器的 API(用 API key/mTLS/短命 token)。
- ★ 無法依賴共享儲存(邊緣運算)。
- ★★ 狀態需要跨天存活 → 資料庫會話狀態。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
影響水平擴展:狀態綁在特定伺服器,叢集下通常得靠黏性會話(sticky session)把同一使用者導回同一台。
-
容錯差:伺服器當掉,記憶體中的會話狀態就遺失。
-
記憶體佔用隨線上人數上升,需注意逾時回收。
-
★★ session 一定要設 TTL,且每次存取續期。 沒設 → Redis 記憶體無限成長 →
noeviction讓寫入失敗(服務掛)或allkeys-lru隨機淘汰有效 session(使用者被隨機登出)。 -
★★ 「Redis 掛了怎麼辦」必須事先決定(全部視為未登入 vs 回 503),不要在事故當下決定。
-
★★
sticky session是最容易後悔的選擇:把一台 pod 下線 = 那些使用者被登出,而滾動部署就是一直在下線 pod。 -
★★ session 的序列化格式會變。用 json、放 version,反序列化失敗就當未登入而不是 500。
-
★★ 併發寫入同一個 session 會互相覆蓋。 session 只放「不會併發改的東西」,或用 WATCH/Lua。
-
★★ session ID 要用
crypto/rand,登入成功後要換新的 ID(防 session fixation)。 -
★★ 不要在 session 裡放權限清單或帳號狀態。改權限不會即時生效,而那常常是安全需求。
-
★ 不要放整個使用者物件,使用者資料改了 session 裡還是舊的。
-
每個請求多一次網路往返是真實成本,考慮在請求作用域內快取一次。
🔗 相關工具
- 用戶端會話狀態 Client Session State(存用戶端,伺服器無狀態易擴展)
- 資料庫會話狀態 Database Session State(存進資料庫,跨伺服器共享且可容錯)
- 企業應用架構模式
- 用戶端會話狀態 —— 無狀態但不可撤銷
- 資料庫會話狀態 —— 持久且可跨天
- 應用控制器 —— 流程狀態的存放選擇
- 登錄器 —— 全域 vs 每使用者
- 樂觀離線鎖 —— session 併發寫入的同一問題