快取是以最小成本獲得最大效能提升的「銀彈」。本文從架構層面深度解析 ASP.NET 快取最佳實務,涵蓋分層快取策略、輸出快取進階技巧、資料快取存取模式、快取失效設計與監控指標,協助建構高效能、可擴充的快取體系。
┌─────────────────────────────────────┐ │ 用戶端快取 (HTTP Headers, ETag) │ ← 瀏覽器/Proxy 層 ├─────────────────────────────────────┤ │ 輸出快取 (OutputCache) │ ← IIS/ASP.NET 頁面層 ├─────────────────────────────────────┤ │ 片段快取 (UserControl OutputCache) │ ← 控制項/區塊層 ├─────────────────────────────────────┤ │ 資料快取 (Cache Object) │ ← 業務/資料存取層 ├─────────────────────────────────────┤ │ 分散式快取 / 資料庫查詢快取 │ ← 叢集/持久層 └─────────────────────────────────────┘
| 層級 | 技術 | 快取對象 | TTL 建議 | 失效觸發 |
|---|---|---|---|---|
| 用戶端 | Cache-Control, ETag, Last-Modified | 靜態資源、API 回應 | 長 (天/月) | 檔案變更、ETag 不符 |
| 頁面輸出 | OutputCache 指令 | 完整 HTML 頁面 | 中 (30秒-5分鐘) | 時間、參數、自訂、相依性 |
| 片段/控制項 | UserControl + OutputCache | 導航選單、側邊欄、Widget | 長 (10-60分鐘) | Shared=true 跨頁共用 |
| 資料/物件 | Cache.Insert, MemoryCache | DTO、List、計算結果 | 視業務而定 | 檔案、SQL、鍵相依、手動 |
| 分散式 | Redis, NCache, AppFabric | Session、熱門資料、Token | 配合業務 | Pub/Sub 失效通知 |
<%@ OutputCache Duration="3600" VaryByParam="categoryId;page;sort" %>
VaryByParam="*" 易導致快取爆炸(每個查詢字串組合各佔一份)<%@ OutputCache Duration="600" VaryByCustom="device" %>
// Global.asax
public override string GetVaryByCustomString(HttpContext context, string custom)
{
if (custom == "device")
{
var browser = context.Request.Browser;
return $"{browser.Platform}_{browser.Browser}_{browser.MajorVersion}";
}
return base.GetVaryByCustomString(context, custom);
}
<!-- Web.config -->
<outputCacheProfiles>
<add name="ProductList" duration="300" varyByParam="categoryId;page" varyByCustom="device" />
<add name="StaticContent" duration="3600" varyByParam="none" />
<add name="UserDashboard" duration="60" varyByParam="*" location="Client" />
<!-- 個人化頁面僅客戶端快取 -->
</outputCacheProfiles>
<%@ OutputCache CacheProfile="ProductList" %>
<%@ Control Language="C#" %>
<%@ OutputCache Duration="600" VaryByParam="none" Shared="true" %>
<asp:Menu ... />
Shared="true":所有引用此 UserControl 的頁面共用同一快取副本<%@ OutputCache Duration="300" VaryByControl="CategoryDropDownList" %>
UserControl 代碼後置需公開屬性:
public int CategoryId
{
get => _categoryId;
set { _categoryId = value; }
}
public T Get<T>(string key, Func<T> loader,
TimeSpan? absoluteExpiration = null,
TimeSpan? slidingExpiration = null,
CacheDependency dependency = null)
{
if (Cache[key] is T cached) return cached;
T value = loader();
var absExp = absoluteExpiration.HasValue
? DateTime.Now.Add(absoluteExpiration.Value)
: Cache.NoAbsoluteExpiration;
var slidExp = slidingExpiration ?? TimeSpan.Zero;
Cache.Insert(key, value, dependency, absExp, slidExp);
return value;
}
// 使用
var customers = CacheHelper.Get("Customers_List", () =>
_repository.GetAllCustomers(),
TimeSpan.FromMinutes(30));
public static class CacheKeys
{
// 格式:[Layer]_[Entity]_[Context]_[ParametersHash]
public static string CustomerList(int? categoryId = null, int page = 1)
=> $"DAL_Customer_List_Cat{categoryId ?? 0}_P{page}";
public static string ProductDetail(int productId)
=> $"DAL_Product_Detail_{productId}";
public static string UserSession(string userId)
=> $"Session_User_{userId}";
}
// 主資料
Cache.Insert(CacheKeys.CustomerList(), customers,
new CacheDependency(Server.MapPath("~/Data/Customers.xml")));
// 衍生資料相依主資料
string[] masterKeys = { CacheKeys.CustomerList() };
Cache.Insert(CacheKeys.CustomerSummary(), summary,
new CacheDependency(null, masterKeys));
// 當 Customers.xml 異動 → CustomerList 失效 → CustomerSummary 連帶失效
Cache.Insert(key, value, null,
DateTime.Now.AddMinutes(30), TimeSpan.Zero,
CacheItemPriority.NotRemovable, // 避免記憶體壓力淘汰
(k, v, reason) =>
{
// 非同步背景重新載入,不阻塞請求
Task.Run(() =>
{
var fresh = LoadData();
Cache.Insert(k, fresh, ...); // 重新插入
});
});
| 策略 | 適用場景 | 實作方式 | 優點 | 缺點 |
|---|---|---|---|---|
| 時間過期 (TTL) | 資料容許短暫過期、讀多寫少 | absoluteExpiration / slidingExpiration | 簡單、無額外機制 | 可能讀到舊資料 |
| 檔案相依性 | 資料來源為 XML/JSON/平面檔 | CacheDependency(filePath) | 即時失效、自動監控 | 僅限檔案系統 |
| SQL 快取相依性 | 資料來源為 SQL Server | SqlCacheDependency | 資料異動即時失效 | 需啟用 Service Broker/查詢通知 |
| 鍵相依性 | 衍生資料、聚合資料 | CacheDependency(null, keys[]) | 建立失效鏈結 | 需手動管理鍵關係 |
| 手動移除 | 寫入操作後立即失效 | Cache.Remove(key) | 強一致性 | 分散式環境需額外同步 |
| 事件驅動失效 | 微服務/分散式架構 | Message Queue + Cache.Remove | 跨程序/跨機器同步 | 複雜度高 |
public class CacheMetrics
{
public long Hits { get; private set; }
public long Misses { get; private set; }
public double HitRatio => Hits / (double)(Hits + Misses);
public T Get<T>(string key, Func<T> loader)
{
if (Cache[key] is T value)
{
Interlocked.Increment(ref Hits);
return value;
}
Interlocked.Increment(ref Misses);
return loader();
}
}
單機快取 (In-Proc Cache) 分散式快取 (Redis / NCache)
┌─────────────┐ ┌─────────────┐
│ Web Server 1│ │ Web Server 1│
│ Cache │ │ │
└──────┬──────┘ └──────┬──────┘
│ │
│ 不同步! │ 共用快取
▼ ▼
┌─────────────┐ ┌─────────────┐
│ Web Server 2│ │ Web Server 2│
│ Cache │ │ │
└─────────────┘ └─────────────┘
HttpRuntime.Cache 替換為 IDistributedCache 介面AppName:Module:Key快取不是「設定後就不管」的功能,而是需要持續監控、調優、演進的系統工程。從單機 OutputCache 起步,到分層快取架構、分散式快取叢集,每個階段都有其適用場景與複雜度權衡。核心原則:測量命中率、設計失效鏈、外部化設定、分層部署、漸進遷移。記住:最好的快取策略,是「不需要快取也能跑得快」的架構——快取是錦上添花,不是雪中送炭。