Skip to main content

Command Palette

Search for a command to run...

Lesson 22: 快取災難預防:快取穿透 、擊穿與雪崩

Updated
3 min read

我們在上一篇文章中介紹了基本的Cache Aside Pattern,也補充了在Database 主從分離架構下可能造成Cache的異常,並一同介紹了:延遲雙刪 以及 CDC。

我們這個章節要來談談Cache還有哪些問題

快取穿透 (Cache Penetration)

定義

請求的資料 不在快取中,也不在資料庫中。 每次請求都會穿過快取,直接打到 DB,但 DB 也查不到資料,導致無法回寫快取。如果有惡意攻擊者使用大量不存在的 ID 進行攻擊,DB 會瞬間承受巨大壓力。

常見場景

  • 惡意攻擊:使用 id = -1 或隨機生成的 UUID 發起大量請求。

  • 程式邏輯錯誤:前端送出了後端根本沒有的資料查詢。

解決方案

  1. 快取空物件 (Cache Null Value):

    • 當 DB 查不到資料時,仍在 Redis 中紀錄該 Key,但值設為 null 或特定標記,並設定一個較短的過期時間 (例如 30 秒)。

    • 優點: 實作簡單。

    • 缺點: 會浪費 Redis 空間存垃圾資料;且在過期前可能會有資料不一致問題 (若該資料突然被新增了)。

  2. 布隆過濾器 (Bloom Filter):

    • 在請求進入快取之前,先透過 Bloom Filter 判斷該 Key 是否「可能存在」。如果 Bloom Filter 說不存在,則直接攔截,不查 Redis 也不查 DB。

    • 優點: 記憶體佔用極少,效率極高,適合超大數據集。

    • 缺點: 實作複雜,存在極低機率的誤判 (False Positive),且刪除資料困難。

  3. 嚴格的參數校驗:

    • 在 Controller 層就攔截不合法的參數 (如 ID < 0)。

補充說明 - 布隆過濾器

Bloom Filter 是一種 「機率型資料結構」

它的核心價值在於:用極小的記憶體空間,快速判斷一個元素「絕對不存在」或「可能存在」。

核心原理:BitMap + 多重雜湊 (Multiple Hashing)

有一個長度為 m 的位元陣列 ,初始值全為 0。 當要儲存一個資料(例如 user_id: 1001)時:

  1. 使用 k 個不同的雜湊函數 對該資料進行運算。

  2. 算出 k 個位置索引 (Index)。

  3. 將 Bit Array 中這 k 個位置的值都設為 1

查詢流程

當一個請求來查詢 user_id: 9999:

  1. 用K個雜湊函數運算。

  2. 檢查這 k 個位置的 Bit 是否全為 1

    1. 情況 A: 只要有 任何一個 位置是 0 →代表該資料 絕對不存在 (直接攔截,不查 DB)。

    2. 情況 B: 所有位置都是 1 → 代表該資料 可能存在 (放行,去查 Cache/DB)。

為什麼會有誤判?

這就是「雜湊碰撞 (Hash Collision)」的代價。 假設 Key A 把位置 1, 5, 7 設為 1。 假設 Key B 把位置 2, 5, 8 設為 1。 現在來了一個不存在的 Key C,它的雜湊結果剛好是 1, 2, 8。 系統一查,發現 1, 2, 8 全部都是 1(由 A 和 B 湊出來的),於是 Bloom Filter 誤以為 Key C 存在。

誤判率與 陣列長度 (m) 和 雜湊函數數量 (k) 有關。陣列越長,誤判越低;k 越多,誤判越低(但也越慢)。

無法刪除: 標準的 Bloom Filter 不支援刪除。因為你不知道把某個位置的 1 改回 0 時,會不會誤傷到其他也共用該位置的 Key。

進階解法: 若需刪除,需使用 Counting Bloom Filter (每個位置不存 Bit,改存 Counter),但空間成本會暴增。

實務選擇

在開發中,我們通常不需手寫 BitMap,而是使用現成的 Redis Module (RedisBloom) RedisBloom - Redis 官方模組,指令如 BF.ADD, BF.EXISTS,效能極佳。

快取擊穿 (Cache Breakdown)

定義

針對 「單一個」 非常熱門的 Key (Hot Key),在這個 Key 過期的瞬間,同時有大量的併發請求進來。 因為快取剛好失效,所有請求瞬間打到 DataBase,就像在盾牌上鑿了一個洞。

常見場景

  • 電商秒殺活動的商品頁。

  • 熱門新聞或話題的 API。

解決方案

  1. 互斥鎖 (Mutex Lock / Distributed Lock):

    • 當發現 Cache 失效時,不是所有執行緒都去查 DB。而是使用 SETNX (Redis) 搶鎖。

    • 搶到鎖的人去查 DB 並回寫 Cache,其他人在旁邊等待並重試讀取 Cache。

    • 優點: 保證資料一致性,DB 壓力最小。

    • 缺點: 程式碼複雜度增加,可能會稍微降低吞吐量。

  2. 邏輯過期 (Logical Expiry / Soft TTL):

    • Redis Key 設定為「永不過期」,但在 Value 內部包含一個邏輯上的「過期時間欄位」。

    • 取出資料時,若發現邏輯時間已過期,則開啟一個「非同步執行緒」去背景更新資料,當下先回傳舊資料給使用者。

    • 優點: 使用者體驗好,幾乎無延遲。

    • 缺點: 需要額外的記憶體存時間戳;在更新完成前,使用者會看到短暫的舊資料。

補充說明 - 互斥鎖

當快取失效,只允許一個 Thread 去重建快取,其他 Thread 等待。

這看似簡單,但實作上有兩個經典的「坑」 - 死鎖 (Deadlock) 與 誤刪鎖。

流程設計

我們通常使用 Redis 的 SETNX (Set if Not Exists) 來實作分散式鎖。

  1. 查詢 Cache: 沒資料 (Miss)。

  2. 爭奪鎖: 嘗試 SET lock_key unique_id NX PX 10000 (設定 10 秒過期)。

    1. 成功 (Got Lock): 查詢 DB → 寫入 Cache → 釋放鎖 (DEL)。

    2. 失敗 (Lock Busy): 休眠 50ms(舉例) → 重試 (Retry)。

關鍵細節

  1. 雙重檢查鎖:

    當等待的 Thread 搶到鎖時,不要直接查 DB。因為在它等待的期間,前一個持有鎖的人可能已經把資料寫進 Cache 了。正確邏輯: 搶到鎖 → 再查一次 Cache → 若有資料直接回傳;若無資料才查 DB

  2. 鎖的原子性與過期:

    • 錯誤寫法: 先 SETNXEXPIRE。如果程式在 SETNX 後當機,EXPIRE 沒執行,這個鎖就變成「永不過期」,造成死鎖。

    • 正確寫法: 使用 Redis 的原子指令 SET key value NX PX milliseconds 一步到位。

  3. 誰加鎖,誰解鎖:

    • 場景: Thread A 搶到鎖,但 DB 查詢太慢,過了 10 秒鎖自動過期了。Thread B 此時搶到鎖開始執行。突然,Thread A 做完了,執行 DEL。結果 A 刪掉了 B 的鎖!

    • 解法: SET 的 value 必須是一個 UUID (Request ID)。解鎖時,先檢查 Value 是否等於自己的 UUID,若是才執行 DEL。這通常需要用 Lua Script 來保證「檢查 + 刪除」的原子性。

快取雪崩 (Cache Avalanche)

定義

雪崩是指 「大量」 的 Key 在 同一時間集體過期,或者 Redis 節點當機。 這會導致原本由 Redis 承擔的海量請求,瞬間全部轉移到 DB,造成 DB CPU 飆升並當機。

常見場景

  • 系統剛重啟,預熱了大量資料,並設定了相同的過期時間 (例如都是 1 小時)。

  • Redis Master 節點掛掉。

解決方案

  1. 隨機過期時間 (Random TTL):

    • 在設定過期時間時,不要設為固定值。例如:原定 60 分鐘過期,改成 60 分鐘 + 隨機 0~5 分鐘。讓失效時間分散開來。
  2. 高可用架構 (High Availability):

    • 使用 Redis Sentinel 或 Redis Cluster。當主節點掛掉時,從節點能自動接手,避免全盤崩潰。
  3. 限流與降級 (Rate Limiting & Circuit Breaker):

    • 當偵測到 Redis 或 DB 壓力過大時,啟動斷路器 (如 Hystrix),直接拒絕部分請求或回傳預設值,保全系統核心功能。
  4. 多級快取 (Multi-Level Cache):

    • 在 Nginx 或應用程式記憶體 (Local Cache, 如 Guava/Caffeine) 增加一層快取。即使 Redis 掛了,本地快取還能擋一陣子。

整理

問題關鍵點發生原因核心解法
穿透 (Penetration)查無此人資料在 Cache 和 DB 都不存在Bloom Filter、快取空物件 (Null)
擊穿 (Breakdown)單點突破單一熱點 Key 過期互斥鎖 (Mutex)、邏輯過期
雪崩 (Avalanche)全面崩盤大量 Key 同時過期 或 Redis 當機隨機 TTL、Redis Cluster、限流降級

從Rookie到Junior,一個後端成長的30堂課

Part 5 of 31

“從 Rookie 到 Junior:一個後端成長的 30 堂課” 是一套專為後端新手所設計的成長型技術系列文章。內容以實務為導向,逐步拆解後端工程的核心能力,包括程式語言基礎、架構思維、框架運作原理、業務邏輯設計、資料庫操作、以及常見的開發模式。 本系列的目標是協助讀者從零散的學習堆疊,建立成體系的後端知識框架。讀者能夠理解各語言背後不變的工程思維與設計原則。這套內容旨在讓學習者從「會寫程式」進入「能理解系統設計」的階段,逐步具備勝任 Junior Backend Engineer 的能力。

Up next

Lesson 21: 空間換取時間-Cache 的快取策略 (Cache-Aside / Delayed Double Delete)

在後端開發的世界裡,有一句至理名言:「沒有什麼效能問題是加一層 Cache 不能解決的;如果有,那就加兩層。」 雖然是句玩笑話,但道出了後端架構的核心思想——「以空間換取時間」。資料庫(Database)存放在硬碟,讀取速度慢且 I/O 成本高;而快取(Cache, 如 Redis)存放在記憶體(RAM),讀取速度極快但空間昂貴。 什麼是 Cache-Aside Pattern? Cache 不會主動和 Database 溝通,所有的數據流動都由應用程式的程式碼來控制。 這就像去圖書館借書: ...

More from this blog

Lesson 26 : 系統韌性的守護者-限流、熔斷與背壓的設計模式

當這幾個名詞出現後,代表我們進到了一個高併發/大流量的系統了。在這個章節中,我們一起來看看如何透過一些方式來避免高併發導致我們的系統crash掉。 限流(Rate Limiting) 相信大家對這個名詞並不陌生,限流其實就是字面上的含意,限制流量。 限流的目的是保護「接收方」,確保系統不會因為瞬間的高併發請求而癱瘓。 限流通常發生在 API Gateway 或服務的最前端。它像是一個夜店門口的保全

Mar 26, 20262 min read

Lesson 25: 淺談 單體架構、微服務架構與單/多租戶架構

過去我們討論了 要把程式寫在哪、程式要怎麼拆的題目,接下來我們來看看「如何服務不同客戶」,這些架構反映了軟體開發在擴充性與複雜度之間的權衡。 軟體架構深度解析:從系統拆分到商業規模化 在軟體工程的演進中,架構的選擇往往是在「開發效率」、「系統擴充性」與「營運成本」之間尋求平衡。我們可以從兩個核心維度來觀察這些架構:系統如何運行(單體 vs. 分散式) 以及 如何服務客戶(單租戶 vs. 多租戶)。

Mar 26, 20262 min read

Lesson 24: 資料庫擴展術-讀寫分離、複寫機制與快取一致性挑戰

為什麼要讀寫分離? 大多數的 Web 應用都是 「讀多寫少」(例如:看文的人多,發文的人少,Heavy Read System)。當所有的請求都塞給同一台資料庫時,磁碟 I/O 和連線數會成為瓶頸。 Master (主庫): 負責寫入 (Insert/Update/Delete),確保數據一致性。 Slave (從庫): 負責讀取 (Select),可以有多個從庫來分擔讀取壓力。 為什麼讀

Mar 25, 20262 min read

面試經驗談 2025-2026

從2025年3月開始,我陸陸續續參與了從新創到上市櫃公司的Senior - Tech Lead的相關面試,其中有不乏 尊重面試者、展現高度專業的企業(公司),當然也有遇到幾場面試鬼故事,這篇文章主要分享我對於軟體工程師面試的方向分享,以及部分鬼故事,以此警惕自己不要成為這樣的面試官。 AI的洪流,改變了SWE的生態 LLM的發展確確實實的影響到了軟體工程師的生態。 過去受限於算力與資料規模,深度學

Mar 23, 20262 min read

Lesson 23: 系統的緩衝區-Queue 佇列與非同步處理 (Asynchronous)

佇列 佇列的實作工具非常多,舉凡AWS SQS、RabbitMQ、Kafka…等。 佇列的特性,其實是一個非常強大的系統緩衝區,應用層面非常廣。 什麼是佇列? 佇列可以想像成,在既有流程中外,有另一個”水管”,來連接原有的資料流(或邏輯過程),其中 呼叫方將資料 推(Push)到水管中,接受方(監聽) 從水管中將資料拉(Pull)出處理 為什麼佇列是「強大的緩衝區」? 在同步處理中,系統像是一

Mar 23, 20262 min read

Bennett's Tech Blog | 後端架構、系統設計

32 posts

來自台灣的軟體工程師,相信軟體可以改變世界