在記憶體資源有限的系統中,快取的大小永遠無法無限膨脹。當快取容量達到上限時,必須有一套智慧演算法來決定:「當新資料進入時,哪一條舊資料應該被立即驅逐(Evict)?」

快取淘汰演算法的良窳,直接決定了快取的命中率(Hit Ratio)。在海量高併發場景下,快取命中率從 90% 提升至 99%,意味著穿透到底層資料庫的流量被瞬間壓低了整整 10 倍!

然而,經典的 LRU 面臨突發全表掃描的快取污染危機,而 LFU 則受限於歷史頻率殘留與巨大的記憶體開銷。

本文將從資料結構、時間與空間複雜度出發,深度剖析從 LRU、LFU、ARC 到現代 Java / Go 生產級快取(如 Caffeine、Ristretto)普遍採用的 W-TinyLFU。


1. 四大快取淘汰演算法全景對決

評估維度LRU(最近最少使用)LFU(最不常使用)ARC(自適應替換)W-TinyLFU(視窗微 LFU)
核心判斷維度僅考慮時間衰減(Recency)僅考慮存取次數(Frequency)動態自適應 Recency + Frequency準入過濾 + 分段衰減
突發掃描污染防禦極差(全量掃描沖刷熱點)良好(熱點不易被剔除)極佳(動態調整邊界)完美防禦(基於準入頻率 PK)
歷史過期頻率適應性良好(新資料隨時間上位)差(舊熱點長期佔用)良好完美(週期性計數減半衰減)
元數據記憶體開銷每個項目 2 個指標(雙向鏈表)需維護計數器或 Min-Heap需維護 4 條鏈表(2x 開銷)極微(每個 Key 僅需 4 bits)
時間複雜度 (Get/Put)O(1)O(1) 或 O(log N)O(1)O(1)
工業級代表實踐Redis(近似 LRU)Redis LFUZFS 檔案系統Caffeine(Java)、Ristretto(Go)

2. 經典 LRU 的原理與突發掃描污染危機

2.1 LRU 資料結構:HashMap + 雙向鏈表

LRU 雙向鏈表與 HashMap 索引結構圖展示最新存取節點移至 Head(最安全),最久未存取節點移至 Tail(滿額淘汰),並透過 HashMap 達成 O(1) 直達索引與位移。Head (最新存取)最安全・不淘汰Node AKey: "user:1"Node BKey: "user:2"Tail (最舊存取)容量滿額 ➔ 優先驅逐!HashMap 索引 (Key ➔ Node 指標):O(1) 定位目標並重置至 Head
HEAD (MOST RECENT)

鏈表頭部:最新被存取節點

無論新寫入或舊資料被讀取,一律以 O(1) 移至 Head。處於最安全防禦區,永不優先淘汰。

↕ 雙向指標鏈結 (Prev / Next)
MIDDLE NODES

中間資料節點 (Node A / B ...)

維護前後節點雙向記憶體指標,被命中時透過 HashMap O(1) 摘除並重新插回 Head。

↕ 雙向指標鏈結 (Prev / Next)
TAIL (LEAST RECENT)

鏈表尾部:最舊存取 (驅逐目標)

長時間未被存取的資料逐漸沉降至 Tail;當快取容量超過上限時,直接從尾部驅逐拋棄。

HASHMAP INDEX

HashMap O(1) 直達索引

底層雜湊表直接映射 Key 到鏈表節點指標,擺脫 O(N) 遍歷開銷,保證讀寫均為 O(1)。

圖:LRU 核心資料結構 —— HashMap O(1) 索引搭配雙向鏈表動態頭尾重置
  • 每次存取或寫入某個 Key 時,將該節點從雙向鏈表中摘下,重新插入到鏈表頭部(Head)。
  • 容量滿時,直接將鏈表尾部(Tail)的節點移除。

2.2 致命缺陷:突發全表掃描污染(Scan Resistance Failure)

假設某後台程式在凌晨發起了一次全庫匯出(遍歷 100 萬筆只會被存取一次的冷資料):

  • 這 100 萬筆冷資料會瞬間湧入 LRU 鏈表頭部,將所有正在高頻存取的線上熱點商品全部強制擠出尾部!
  • 匯出結束後,原本命中率高達 99% 的快取瞬間暴跌至 0%,引發毀滅性的資料庫連鎖崩潰。

3. LFU 的原理與歷史頻率殘留陷阱

LFU(Least Frequently Used)根據每個資料項目的歷史存取次數進行排序,優先淘汰存取次數最少的資料。

3.1 致命缺陷:歷史熱點殭屍化(Frequency Pollution)

  • 假設某篇新聞在昨天被瘋狂點擊了 10 萬次,其頻率計數高達 100,000。
  • 到了今天,該新聞熱度歸零,再無人問津;但因為其計數高達 10 萬,任何今天新發布的爆款新聞(計數只有 1~5 次)在快取滿時都會被無情拒絕或立即淘汰,而這篇殭屍新聞將長期霸佔快取空間。

4. 現代顛覆者:Window TinyLFU (W-TinyLFU)

為了徹底結合 LRU 對時間衰減的敏感度與 LFU 對長期頻率的精確判斷,Ben Manes 在 Caffeine 快取 中設計了 W-TinyLFU 演算法。

W-TinyLFU 現代快取架構:Window LRU、TinyLFU 準入過濾與分段 SLRU 展示 Caffeine 快取如何透過 1% Window LRU 捕捉突發熱點,經由 4-bit Count-Min Sketch 頻率對決,決定資料晉升至 99% SLRU 主快取或直接淘汰。01. INGRESS新資料緩衝區新增項目寫入• 未命中時載入記憶體• 預設進入 Window LRUWindow LRU (1%)• 保留短暫突發存取緩衝• 避免立即淘汰新資料• 滿額產生 Candidate 挑戰者掃描污染防禦• 冷資料全表掃描時• 僅在 1% 視窗內打轉02. ADMISSIONTinyLFU 準入閘門Count-Min Sketch• 4-bit 概率計數矩陣 (0~15)• 空間極度壓縮 (每項 4 bits)• 週期性計數折半衰減• 徹底根治 LFU 歷史熱點殭屍化頻率擂台 PK 規則• 挑戰者:Window 淘汰候選• 守擂者:主快取最末尾節點Freq(新) > Freq(舊) ➔ 准入Freq(新) ≤ Freq(舊) ➔ 丟棄03. MAIN CACHE分段主快取 (99%)✔ 晉升 Segmented LRU• 試用區 (Probationary: 20%)• 再次命中晉升至保護區• 保護區 (Protected: 80%)✔ 保障高頻真實熱點安全✖ 頻率不足 (Evicted)• 挑戰失敗直接丟棄• 主快取熱點完全不被污染Caffeine 工業級實踐• 命中率與吞吐量最佳化
01. INGRESS & WINDOW LRU

新資料緩衝區 (容量佔比 1%)

未命中資料優先載入小容量 Window LRU,捕捉短暫的高頻突發請求;當突發全表掃描發生時,冷資料僅在此處流轉,絕不直接衝擊主快取。

↓ 滿額產生挑戰者 Candidate
02. TINYLFU ADMISSION FILTER

Count-Min Sketch 頻率對決

採用 4-bit 概率計數矩陣估算存取頻率(每項僅 4 bits 開銷),搭配定期折半衰減避免殭屍熱點;將 Window 淘汰者與主快取末尾者進行頻率 PK。

↓ 頻率較高者晉升
03A. SLRU PROMOTION (晉升主快取)

晉升 Segmented LRU (佔比 99%)

挑戰成功的熱點進入 20% 試用區(Probationary),若再次被讀取則升入 80% 保護區(Protected),確保高頻核心熱點長效存留。

↓ 頻率不足者淘汰
03B. DROP (拒絕准入)

頻率不足:直接丟棄

無法勝過主快取末尾項的冷資料直接被驅逐,從根源徹底免疫突發冷資料的全表掃描污染。

圖:現代快取淘汰架構 —— W-TinyLFU 雙層快取、4-bit Count-Min Sketch 準入與 SLRU 分段防禦

4.1 核心三大創新機制

  1. TinyLFU 準入策略(Admission Policy):
    • 當新資料被擠出 Window LRU 時,它不能直接進入主快取,而是作為「挑戰者」與主快取中即將被淘汰的「守擂者」進行頻率 PK。
    • 只有挑戰者的存取頻率大於守擂者時,才允許其晉升進入主快取,否則直接拒絕。這從根本上免疫了突發全表掃描對快取的污染!
  2. Count-Min Sketch 空間極限壓縮:
    • 如果為每個 Key 保存一個 32 位元或 64 位元的計數器,快取中元數據的體積將超過資料本身。
    • TinyLFU 採用 4-bit Count-Min Sketch(基於 Bloom Filter 思想的概率計數矩陣),每個 Key 僅需 4 bits(最大計數到 15),即能以極小的記憶體開銷精確估算頻率。
  3. 週期性衰減(Reset / Aging Mechanism):
    • 當累積存取次數達到樣本總量上限時,TinyLFU 會將所有計數器的值除以 2(右移 1 位元)。
    • 讓歷史熱點的權重隨時間自動減半衰退,完美解決了傳統 LFU 的殭屍資料問題!

5. 架構選型總結

  • Java 應用本地快取:一律強制採用 Caffeine(內建 W-TinyLFU),其吞吐量與命中率在所有 Benchmark 中均全面碾壓 Guava Cache 與 Ehcache。
  • Go 語言本地快取:採用 Ristretto(Dgraph 團隊基於 TinyLFU 打造)。
  • 分散式 Redis:生產環境推薦配置 volatile-lru 或 allkeys-lfu,結合合理記憶體上限進行平滑淘汰。