身分服務
為多租戶平台提供帳號、Session 與多層級合作夥伴架構
專案背景
平台上的其他所有服務都仰賴身分資訊:使用者是誰、屬於哪個租戶、在最深可達十層的合作夥伴層級中位於何處。它必須能快速回答「X 的所有下層成員」,確保 Session 可被撤銷,並在帳號建立橫跨多個服務時維持一致。
架構設計
閘道 → 身分服務(go-zero gRPC)→ 透過由可組合 GORM scope 組成的 db/ 層存取 MySQL;Redis 位於 cache/ 層之後,由 cacheSync/ 層協調 read-through 與非同步回寫。帳號變更連同變更前後快照與 trace header 一併發布到 Kafka,讓 CRM 讀取模型與客戶分群服務得以跟進。
核心技術亮點
以閉包表(祖先、後代、深度)實作 10 層合作夥伴網路的層級結構,並在建立帳號的同一筆資料庫交易中寫入
三層資料存取:MySQL → Redis → 非同步快取同步,搭配扇出 worker、singleflight 與 TTL 隨機抖動
以 Redis 布隆過濾器進行帳號去重;JWT Session 支援滑動續期與伺服器端撤銷
抽離為獨立的 gRPC 微服務(拆分時共 92 個 RPC);之後再新增約 20 個 RPC,包括基於閉包表的層級查詢
跨服務建立帳號時的補償回滾;所有步驟都成功後才發布註冊事件
快取層強化:附佇列已滿指標的有界非同步 worker 池、Session 撤銷的同步備援,以及快取未命中時的批次載入
核心實現
以閉包表實作合作夥伴層級
儲存每一組祖先–後代關係及其深度,與建立帳號在同一筆交易中寫入,並在此基礎上建立子樹、下層成員與各租戶目錄查詢。
Why: 子樹查詢(「這個合作夥伴底下的所有人」)是最常見的讀取。閉包表只需一次索引查詢即可回答,不必使用遞迴查詢;深度欄位也讓 10 層上限容易落實。
具非同步同步的三層快取
快取未命中時,讀取經由 singleflight 存取資料庫。快取回寫與失效在附 TTL 隨機抖動的有界非同步 worker 池中執行。2025 年我進一步強化:修正未命中的處理邏輯、將 singleflight 限縮在資料庫讀取、加入每次呼叫的逾時與佇列已滿計數器,並在 worker 池飽和時讓 Session 撤銷改以同步方式執行。
Why: 身分服務位於每一條請求路徑上。合併並行的未命中、分散過期時間、絕不遺漏任何一次撤銷,在這裡比任何地方都重要。
Result: 將逐一使用者的快取未命中查詢,改為一次批次查詢加一次 pipeline 寫入。
跨服務建立帳號的一致性
建立帳號時,也會在其他服務中建立錢包與設定。每個流程都拆成驗證、準備與初始化步驟;若後續步驟失敗,則在獨立且具逾時保護的 context 上執行補償刪除。
Why: 若沒有這套機制,下游失敗會留下建立到一半的使用者,下游消費者也可能收到一個早已不存在之帳號的註冊事件。
技術決策
| 技術決策 | 選擇 | 替代方案 | 原因 |
|---|---|---|---|
| 層級儲存方式 | 閉包表 | 鄰接串列 + 遞迴查詢 | 讀取次數遠多於層級異動,而且幾乎每次讀取都是子樹查詢。插入時多寫幾列,換來子樹查詢只需一次查找。 |
| Session 撤銷 | 在 JWT 之外,於 Redis 中為每位使用者保存一個有效 token | 純無狀態 JWT | 管理員必須能立即停用帳號並強制登出;無狀態 token 在到期前無法撤銷。 |