🎯 什麼情境該想到我
當你「要讓會話狀態跨多台伺服器共享、且伺服器當掉也不遺失」的時候。
⚙️ 怎麼用(步驟 / 公式)
意圖:把會話狀態存進資料庫,讓任一伺服器都能讀寫同一份狀態。
做法要點:
- 每個請求進來,用 session id 從資料庫讀出所需狀態;處理完再寫回。
- 狀態集中在資料庫,任一台伺服器都能接手,不需黏性會話。
- 資料庫的持久性帶來容錯:伺服器故障不影響已保存的會話。
★★ 它的真正價值:狀態變成「可查詢的業務資料」
★ 三個只有資料庫會話狀態能做到的事:
① ★★ 中斷可續 —— 使用者三天後回來繼續填(session 早就過期了)
② ★★ 漏斗分析 —— 「有多少人卡在第三步」是產品最想知道的問題
→ ★ Redis 裡的 session 查不到;資料庫裡一個 GROUP BY 就有
③ ★ 稽核與客服 —— 「使用者說他填過了」→ ★ 你能查出他到哪一步、什麼時候
★★ 關鍵洞見:一旦「會話狀態」需要被查詢或分析,它就不再是會話狀態,
而是一筆業務資料。
★ 那時它該有自己的表、自己的 schema、自己的生命週期——
而不是一個塞在 session 裡的 blob。
★★ Go 的實作:把它當成一個真正的實體
-- ★★ 不要用「一張 sessions 表 + 一個 jsonb blob」
CREATE TABLE sessions (id text PRIMARY KEY, data jsonb); -- ★ 這只是把 Redis 搬到 DB
-- ✅ ★ 明確的業務實體
CREATE TABLE signup_applications (
id text PRIMARY KEY,
user_id text, -- ★ 可為 NULL(還沒建帳號)
state text NOT NULL, -- ★ 狀態機(見 [[應用控制器]])
basic_info jsonb,
profile jsonb,
version bigint NOT NULL DEFAULT 1, -- ★★ 樂觀鎖
created_at timestamptz NOT NULL DEFAULT now(),
updated_at timestamptz NOT NULL DEFAULT now(),
expires_at timestamptz NOT NULL -- ★★ 沒有這個就會無限成長
);
CREATE INDEX ON signup_applications (state, updated_at); -- ★ 漏斗分析用
CREATE INDEX ON signup_applications (expires_at); -- ★ 清理用★★ expires_at + 清理排程不可省略:
會話資料只增不減 → ★ 一年後這張表有幾千萬列廢資料,
而且它會拖慢所有針對它的查詢(包含漏斗分析)。
★★ 三個必須處理的問題
① ★★ 併發寫入
使用者在兩個 tab 同時填 → ★ 後寫的覆蓋前者
→ ★ 用 version 欄位做樂觀鎖(見 [[樂觀離線鎖]]),衝突時明確告知使用者
② ★★ 清理
★ 不要用「查詢時忽略過期的」取代刪除 —— 表還是會無限成長
→ ★ 一個 DELETE … WHERE expires_at < now() 的排程
→ ★★ 但要分批(LIMIT + 迴圈),否則一次刪幾百萬列會鎖住整張表
③ ★★ 未完成的資料算不算「真的存在」
使用者填了 basic_info 但沒完成註冊 → ★ 那個 email 算不算已被使用?
→ ★★ 這是業務決定,而且必須明確:
a. 不算(唯一索引只建在完成的帳號上:partial unique index)
b. 算(佔位,但要有過期釋放)
→ ★ 沒有明確決定 → ★★ 兩個人可以用同一個 email 完成註冊
★★ ③ 是最常被忽略、後果最嚴重的一個——它是一個資料完整性問題,
而不是「會話管理」問題。
★★ 什麼時候用它 vs 用其他兩種
| 用戶端會話狀態 | 伺服器會話狀態 | 資料庫會話狀態 | |
|---|---|---|---|
| 存活時間 | ★ token 有效期 | ★ 分鐘到小時 | ★★ 天到永久 |
| 可撤銷 | ❌ | ✅ | ✅ |
| 可查詢/分析 | ❌ | ★ 有限 | ★★ 完整 |
| 每請求成本 | ★ 零(或驗簽) | ★ 一次網路往返 | ★★ 一次 DB 查詢 |
| 狀態大小 | ★ 4 KB 上限 | ★ 幾 KB–MB | ★★ 無實際上限 |
| ★ 適合 | 短命 access token | ★ 登入狀態 | ★★ 多步驟業務流程 |
★★ 實務上三者並存:
★ 登入身分 → 短命 JWT([[用戶端會話狀態]])+ session_version 查表
★ 登入 session 與 CSRF token → Redis([[伺服器會話狀態]])
★★ 多步驟申請、購物車、草稿 → 資料庫(本頁)
★ 判準:這個狀態「遺失了使用者會生氣嗎」?
會(填了二十分鐘的申請書)→ 資料庫。
不會(登入狀態,重新登入即可)→ session。
★ 兄弟辨析
🧪 我實際套用的紀錄
- (待填)
⚠️ 注意 / 什麼時候不適用
-
代價是每次請求都要讀寫資料庫,增加負載與延遲。
-
需處理暫時性(會話中)與已提交資料的區隔,避免污染正式資料。
-
要有清除過期會話的機制,否則資料表會膨脹。
-
★★ 一旦會話狀態需要被查詢或分析,它就是一筆業務資料,該有自己的表與 schema,不是一個 jsonb blob。
-
★★
expires_at+ 清理排程不可省略,否則一年後幾千萬列廢資料拖慢所有查詢。 -
★★ 清理要分批(
LIMIT+ 迴圈),一次刪幾百萬列會鎖住整張表。 -
★★ 「未完成的資料算不算存在」必須明確決定(那個 email 算不算被使用)。沒決定 → 兩個人可以用同一個 email 完成註冊。
-
★★ 併發寫入(兩個 tab)會互相覆蓋。 用 version 欄位做樂觀鎖,衝突時明確告知使用者。
-
★ 不要用「查詢時忽略過期的」取代刪除,表還是會無限成長。
-
★ 判準:這個狀態遺失了使用者會生氣嗎? 會(填了二十分鐘的申請書)→ 資料庫。
-
每個請求多一次 DB 查詢是真實成本,不要把登入狀態放這裡。
-
三種會話狀態實務上並存,不是互斥選擇。
🔗 相關工具
- 伺服器會話狀態 Server Session State(存伺服器記憶體,最簡單但難擴展)
- 用戶端會話狀態 Client Session State(存用戶端,伺服器無狀態)
- 企業應用架構模式
- 用戶端會話狀態、伺服器會話狀態 —— 另兩種存放位置
- 應用控制器 —— 多步驟流程的狀態機
- 樂觀離線鎖 —— 併發寫入的對策
- 序列化大物件 —— jsonb 的取捨
- 識別欄位 —— 未完成資料的唯一性約束