[設計案例] 清除Cache物件 #1. 問題與作法
摘要提示
- Cache移除困境: 雖有 Remove 可刪單一項目,但實務上難以管理鍵值與群組性移除需求。
- 列舉鍵值限制: Cache 無公開 Keys 屬性,需用 GetEnumerator 巡覽,且存在競態風險。
- 不確定性本質: Cache 項目可隨時被移除,操作時需假設其瞬時性與不可預期。
- 實務案例背景: 以簡易瀏覽器快取為例,按 URL 快取下載內容,衍生選擇性清除需求。
- 清除條件多樣: 需能按網域、Content-Type、Protocol 等條件批次清除。
- 直覺作法代價: 自行維護 URL 索引或用正則掃描 Cache,皆帶來效能與可維護性問題。
- 設計美感考量: 可動不等於設計好;需兼顧效能、簡潔、可讀性與美感。
- 依賴式解法: 以 CacheDependency 綁定「類別對應的檔案」,touch 檔案即可批次失效。
- 聰明與愚蠢並存: 解決群組失效優雅,但引入額外檔案 I/O,恐影響效率。
- 待續實作預告: 本文提出思路與取捨,實作細節與程式碼將於續篇說明。
全文重點
作者從 .NET 的 HttpRuntime.Cache 談起,指出雖然大家對快取已十分熟悉,但在「手動移除特定物件」這件事上,實務仍存在設計與操作的困難。從 API 角度看,Cache.Remove(key) 能刪除單一項目,但會遇到兩個痛點:第一,必須在系統中額外記住並管理鍵值,當要移除的項目多、關聯複雜時,程式會變得醜陋難維護;第二,若想知道所有現存鍵值以便篩選,Cache 並未提供 Keys 屬性,只能透過 GetEnumerator 迭代鍵,但即使拿到鍵值,也可能在下一瞬間因快取機制而失效,存在競態與不確定性。
作者以「簡易瀏覽器」為例,將快取以 URL 為 Key、下載內容為 Value 進行保存。然而當需求升級為「選擇性清除」時,例如按特定網域、Content-Type 或協定批次清除,傳統作法便捉襟見肘。直覺解決方案有二:其一,建置自有的索引與資料結構,維護所有下載過的 URL 與其屬性,以便快速找出要移除的鍵並呼叫 Remove;其二,直接迭代快取中現存的鍵,透過正則或條件過濾後逐一刪除。兩者皆可行,但前者等同自行重造快取機制,且會與快取的即時淘汰狀態失同步;後者則在大量項目時效率低下,且程式風格冗長、不夠優雅。
在追求效能、簡潔、可讀性與設計美感之下,作者提出一個兼具巧思與爭議的做法:利用 CacheDependency 的群組失效能力,先在寫入快取時,依據「欲支援的清除條件」將快取項目分群,並讓每一群與一個對應的實體檔案建立依賴;當需要清除該群組時,只要「touch」對應檔案,即可由快取機制自動將整組關聯項目失效移除。此法優點是:不必事後掃描快取或維護龐大的索引,即可用「外部訊號」一致性地驅動批次失效,設計上整潔而集中。缺點則是引入了額外的檔案系統 I/O,對於原本可在記憶體層面解決的問題,加入最慢的外部操作,可能影響效能與部署複雜度。
本文最終定位在問題定義與設計思路的闡述,而非立即給出完整實作。作者承諾在續篇提供實際程式碼,並表示本文重點在提醒:面對快取的不確定性與群組清除需求,僅靠 Remove 和鍵迭代並不足以優雅解決;應該善用快取的依賴與失效機制,設計出既能批量清除、又不犧牲可維護性的方案。同時,也坦承每個解法都有代價,需要在「優雅」與「效率」之間做出取捨,並依實際情境選擇最合宜的策略。
段落重點
引言與主題設定:從好標題難產到 Cache 操作心得
作者以幽默口吻自嘲難以下標,引出主題:分享 .NET HttpRuntime.Cache 的操作心得。核心問題是:如何手動將某個物件從快取中移除。雖然多數人熟悉快取,但實務應用常踩到設計陷阱,尤其在需要控制性地清除資料時,細節與風險遠比想像多。本文先點出問題,再逐步鋪陳限制與思路,為後續設計案例鋪路。
單項移除容易,鍵值管理困難
API 層面上,使用 Cache.Remove(key) 即可刪除指定項目,看似簡單。然而實務難點在於鍵值管理:系統須為每個快取項目記錄、維護其鍵;當要移除的項目涉及多對多關聯或批次條件,程式便顯得笨重且不雅。此外,快取資料的生命週期不可預期,鍵與資料可能不再同步,使移除邏輯更顯脆弱。
列舉所有鍵:可行但風險高
Cache 不提供像一般 Dictionary 的 Keys 屬性,只能透過 GetEnumerator 迭代現存鍵。此法可讀取目前所有鍵並進行篩選刪除,但快取的不確定性導致競態:即使剛拿到鍵,下一刻對應項目可能已被淘汰。若快取量大,全面掃描也在效能上不夠友善,讓人難以接受作為長期方案。
瀏覽器快取案例與「選擇性清除」需求
以簡易瀏覽器為例:以 URL 作為鍵、將下載內容快取。當使用者需要「部份清除」時,條件可能包括:特定網域、特定 Content-Type、特定協定等。此類需求不僅合理,且在真實產品中極常見。然而單靠 Remove 或鍵迭代,難以優雅支援這些多維度的批次清除行為。
直覺解法評估:自行索引與正則掃描的侷限
兩個常見作法:一是自建索引與資料結構,紀錄所有下載 URL 與屬性,據此挑出鍵並移除;二是遍歷快取鍵,透過正則或條件過濾逐一刪除。前者形同重造快取,且易與快取當前狀態失同步;後者每次都要掃描大量項目,性能與程式美感皆不佳。兩者都可運作,但距離「乾淨、可靠、可維護」仍有落差。
依賴式群組失效:用 CacheDependency 搭配檔案觸發
作者提出一個兼具巧思與妥協的設計:在寫入快取時,先將項目按清除條件分群,為每群關聯一個檔案依賴(CacheDependency)。當要清空該群組,便觸碰(touch)對應檔案,觸發快取自動失效,整組項目同步移除。此法優點在於:批次清除簡潔一致、避免掃描快取、降低鍵管理負擔;缺點是引入檔案 I/O,可能是瓶頸,也增加部署複雜度。作者承諾續篇提供具體程式碼與實務細節。
資訊整理
知識架構圖
- 前置知識:
- .NET/ASP.NET 基本觀念
- HttpRuntime.Cache 與快取概念(到期、移除、不確定性)
- Dictionary/集合迭代與 Enumerator
- 基本正規表達式與字串匹配
- 檔案系統操作(觸發時間戳變更、I/O 成本)
- 核心概念:
- Cache 的不確定性:快取項目可能隨時被移除,需避免假設一致性
- Key-based 移除:以 Cache.Remove(key) 為基本操作但需管理 key
- Key 枚舉:透過 GetEnumerator 迭代所有 key 的方法與風險
- 分群與關聯失效:以群組策略管理「一組」快取的清除需求
- CacheDependency 機制:以檔案依賴作為群組失效的觸發器
- 技術依賴:
- HttpRuntime.Cache 基本 API:Insert/Add、Remove、GetEnumerator
- CacheDependency(檔案相依)→ 需依賴檔案系統 I/O(touch/更新時間)
- 字串策略(URL 作為 key)→ 規劃命名或分群對映
- 正規表達式/篩選邏輯 → 僅能作用於目前仍在快取中的項目
- 應用場景:
- Web 應用程式需清除「部分」快取(按網域、內容型態、通訊協定分組)
- 類瀏覽器資源快取(temporary internet files 的替代)
- 大量快取時的選擇性失效(避免全清造成冷啟動負擔)
- 需提供使用者或系統批次清除某一群快取的功能
學習路徑建議
- 入門者路徑:
- 理解 HttpRuntime.Cache 的基本使用(設定、取得、Remove)
- 學會以 URL 做為 key 的快取策略與到期設定
- 練習使用 GetEnumerator 迭代 key 並安全地操作
- 進階者路徑:
-
設計可維護的 key 命名/分群規則(如前綴:protocol host path) - 評估正規表達式篩選與其效能/一致性風險
- 了解並實作 CacheDependency(以檔案依賴作群組失效)
-
- 實戰路徑:
- 針對「按網域/Content-Type/Protocol」三類清除需求規劃群組
- 為每個群組建立對應的檔案依賴,並在清除時 touch 觸發失效
- 建立工具方法:新增快取時自動掛載群組依賴;清除 API 只負責觸發
- 監控與記錄:統計快取命中率、清除次數與耗時,評估 I/O 成本
關鍵要點清單
- Cache 的不確定性:快取項目可能隨時被清掉,操作要容錯與重試 (優先級: 高)
- 以 key 移除(Cache.Remove):最直接但需額外管理 key 清單 (優先級: 高)
- Key 枚舉(GetEnumerator):能遍歷現存項目,但有一致性與效能風險 (優先級: 高)
- URL 作為 key:直觀易用,但需規劃命名與轉碼避免衝突 (優先級: 中)
- 分群清除需求:按網域、Content-Type、Protocol 等維度清除 (優先級: 高)
- 外部清單管理法:自行維護 URL 清單便於清除,但易與實際快取不同步 (優先級: 中)
- 正規表達式篩選:可從現存快取過濾,但大規模掃描效能差 (優先級: 中)
- CacheDependency(檔案依賴):以觸發檔案變更達成群組失效 (優先級: 高)
- 觸發檔案(touch)策略:可優雅清除整組快取,但引入 I/O 成本 (優先級: 中)
- 一致性考量:遍歷與移除間可能競態,需設計可容錯流程 (優先級: 高)
- 可讀性與美感:避免雜亂掃描與分支,提倡群組化與集中清除 API (優先級: 中)
- 擴充性設計:新增新分群維度時,只需新增對應依賴與映射 (優先級: 中)
- 效能權衡:檔案 I/O 成本 vs. 大量遍歷與字串匹配成本 (優先級: 中)
- 日誌與監控:記錄清除操作、命中率與耗時,持續調整策略 (優先級: 中)
- 失敗處理:清除/觸發失敗時的重試與降級(例如退回全域清除) (優先級: 低)