🎯 什麼情境該想到我

當你「要讓會話狀態跨多台伺服器共享、且伺服器當掉也不遺失」的時候。

⚙️ 怎麼用(步驟 / 公式)

意圖:把會話狀態存進資料庫,讓任一伺服器都能讀寫同一份狀態。

做法要點:

  • 每個請求進來,用 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。

★ 兄弟辨析

差別
用戶端會話狀態、伺服器會話狀態另兩種存放位置
應用控制器★ 多步驟流程的狀態機,常配這個模式
樂觀離線鎖★ 併發寫入的對策
序列化大物件★ 中間資料放 jsonb 的取捨

🧪 我實際套用的紀錄

  • (待填)

⚠️ 注意 / 什麼時候不適用

  • 代價是每次請求都要讀寫資料庫,增加負載與延遲。

  • 需處理暫時性(會話中)與已提交資料的區隔,避免污染正式資料。

  • 要有清除過期會話的機制,否則資料表會膨脹。

  • ★★ 一旦會話狀態需要被查詢或分析,它就是一筆業務資料,該有自己的表與 schema,不是一個 jsonb blob。

  • ★★ expires_at + 清理排程不可省略,否則一年後幾千萬列廢資料拖慢所有查詢。

  • ★★ 清理要分批(LIMIT + 迴圈),一次刪幾百萬列會鎖住整張表。

  • ★★ 「未完成的資料算不算存在」必須明確決定(那個 email 算不算被使用)。沒決定 → 兩個人可以用同一個 email 完成註冊。

  • ★★ 併發寫入(兩個 tab)會互相覆蓋。 用 version 欄位做樂觀鎖,衝突時明確告知使用者。

  • ★ 不要用「查詢時忽略過期的」取代刪除,表還是會無限成長。

  • ★ 判準:這個狀態遺失了使用者會生氣嗎? 會(填了二十分鐘的申請書)→ 資料庫。

  • 每個請求多一次 DB 查詢是真實成本,不要把登入狀態放這裡。

  • 三種會話狀態實務上並存,不是互斥選擇。

🔗 相關工具