背景問題:Cache Stampede(快取雪崩)
在高並發環境下,當某個 Redis cache key 同時過期,大量請求會同時發現 cache miss 並全部打向後端 DB / Provider,這就是 Cache Stampede。
flowchart TD
E[Cache Key 過期] --> A[Request A Cache Miss]
E --> B[Request B Cache Miss]
E --> C[Request C Cache Miss]
E --> D[Request D Cache Miss]
A --> P[Call Provider]
B --> P
C --> P
D --> P
P --> DB[DB 或 Provider 被打爆]
解決方案: 在更新 Cache 時加 分散式鎖(RedLock),確保同一時間只有一個 instance 去 Provider 取資料並回填 Cache。
系統架構
flowchart TD
A[Client Request] --> B[CacheLayerManager GetDataAsync]
B --> C{Redis CacheState Key 存在}
C -->|是| D[FetchFromRedis 直接返回]
C -->|否| E[RedLockActionAsync 取得分散式鎖]
E --> F{Lock 取得成功}
F -->|否 其他 instance 更新中| G[放棄此次更新 返回 default 或待下次請求]
F -->|是| H[DataContract GetDataFromProviderAsync BLL DB]
H --> I[Save Redis Hash redisObjectStore Save]
I --> J[設定 CacheState Key TTL Active]
J --> K[Release RedLock]
K --> D
核心元件
1. RedLockService — 工廠初始化
1 | // RedLockService.cs |
- 使用
RedLockNet.SERedisNuGet 套件 RedLockFactory以OneTimeTokenRedis connection 建立- Singleton pattern,整個 App 共用同一個 Factory
2. RedLock Key 設計
1 | // CacheLayerManager.cs |
Key 格式: {CacheName}:{memberKey}:CacheState
例如:TradingSession:member001:CacheState
RedLock 直接鎖 CacheState Key,因為這個 key 同時也是 cache 是否存在的狀態旗標,語義一致。
3. RedLockActionAsync — 非同步鎖
1 | private static async Task RedLockActionAsync( |
關鍵設計決策:
!redLock.IsAcquired→ 放棄而非等待,避免 thread 堆積- Lock TTL = 30 秒,防止持有者 crash 後鎖不釋放(dead lock)
using確保鎖一定被釋放
Cache 狀態機
stateDiagram-v2
[*] --> NotExist : Key 不存在或過期
NotExist --> Inprogress : RedLock 取得 開始分頁載入
Inprogress --> Active : 列表場景 資料載入完成
NotExist --> Active : 單筆資料更新完成
Active --> NotExist : TTL 過期
Active --> Active : 正常讀取
note right of Inprogress
List 分頁:資料分批 push 進 Redis List
此期間狀態為 Inprogress
end note
CacheStatus Enum:
| Status | 說明 |
|---|---|
NotExist |
Key 不存在,需要初始化 |
Inprogress |
List 場景下,資料正在分批填充 |
Active |
資料完整,可直接讀取 |
兩種 Cache 模式
模式一:單筆資料(Hash 儲存)
適用於 TradingSession, ProfileSession, KYCVerification 等單一物件。
sequenceDiagram
participant C as Client
participant CLM as CacheLayerManager
participant RL as RedLock
participant R as Redis
participant P as Provider_BLL
C->>CLM: GetDataAsync cacheName key query
CLM->>R: KeyExists CacheStateKey
R-->>CLM: false Not Exist
CLM->>RL: CreateLockAsync CacheStateKey 30s
RL-->>CLM: Lock Acquired
CLM->>P: GetDataFromProviderAsync
P-->>CLM: data JSON
CLM->>R: Save CacheDataKey
CLM->>R: SetKeyExpire CacheDataTime
CLM->>R: StringSet CacheStateKey Active
CLM->>RL: Release Dispose
CLM->>R: Get CacheDataKey
R-->>CLM: cached data
CLM-->>C: Deserialized T
實作:
1 | private static async Task UpdateCacheToRedisAsync( |
模式二:分頁列表(List 儲存)
適用於 ImportantNotifications, BankingNotifications 等分頁資料。
| 項目 | 說明 |
|---|---|
| Redis List Key | {CacheName}:{key}:CacheData |
| 結構 | 索引 0, 1, 2… 各元素為一筆序列化資料 |
分批載入流程:
1 | var index = status.Equals(CacheStatus.NotExist) ? 0 : range.startIndex; |
CacheLayerManager 支援的 Cache 項目
| CacheName | 資料類型 | Provider 來源 |
|---|---|---|
Star4KYCVerification |
單筆 | AccountManager |
TradingSession |
單筆 | LoginManager |
ProfileSession |
單筆 | LoginManager |
MemberPromotionsLight |
單筆 | PromotionManagement |
MemberRecentTransactions |
單筆 | TransHistoryManager |
MemberRecentGames |
單筆 | ProductManager |
MemberRecommendGames |
單筆 | ProductManager |
ImportantNotifications |
列表(分頁) | MsgHubManagement |
BankingNotifications |
列表(分頁) | MsgHubManagement |
MyBetsNotifications |
列表(分頁) | MsgHubManagement |
PromotionsNotifications |
列表(分頁) | MsgHubManagement |
PaymentInfo |
單筆 | TransactionContext |
Announcement |
列表(分頁) | MsgHubManagement |
MemberRewards |
單筆(async) | PromotionManagement |
MemberRebates |
單筆 | PromotionManagement |
同步包裝 Async 的注意事項
CacheLayerManager 中有多處 Task.Run(...).GetAwaiter().GetResult() 的模式:
1 | public static T GetData<T>(CacheName cacheName, string key, object queryModel) where T : new() |
為什麼這樣做:
flowchart TD
A[ASP.NET classic 有 SynchronizationContext] --> B[若在 SyncContext 上 GetResult 阻塞]
B --> C[await 延續等待 SyncContext]
C --> D[死結 SyncContext 被 GetResult 佔住]
S[Task.Run 將 async 工作丟到 ThreadPool] --> E[ThreadPool 無 SyncContext]
E --> F[延續不需回灌原 SyncContext 避免死結]
設定:CacheLayerManager.config
1 | <ArrayOfCacheLayerInstance> |
CacheStatusTime < CacheDataTime 的設計:
- Status Key 先過期 → 下次請求觸發重新填充
- Data Key 後過期 → 更新期間仍可讀取舊資料(防止空白期)
總結:RedLock 在此系統的角色
flowchart TB
subgraph SRV["多台 App Server"]
SA[Server A]
SB[Server B]
SC[Server C]
end
RD[Redis]
SA --> RD
SB --> RD
SC --> RD
RD --> LK["Lock Key 例 TradingSession member001 CacheState"]
LK --> R1["同一時間僅一台可持有 RedLock"]
LK --> R2["其餘 IsAcquired false 直接放棄更新"]
設計原則:
- Lock on CacheStateKey — 鎖的語義和 cache 狀態旗標一致
- Lock TTL = 30s — 防止 dead lock,長於正常更新時間
- Fail-fast(非等待) —
!IsAcquired直接 return,避免 thread 堆積 - ConfigureAwait(false) — 避免 ASP.NET SynchronizationContext 死鎖