目錄
一、系統概述
二、名詞解釋
三、淘汰策略
1、LRU
2、LFU
3、FIFO
4、TTL
5、Random
四、讀寫模式
1、Cache Aside(旁路緩存)
2、Write Through(直寫)
3、Write Back(回寫)
五、問題方案
1、緩存穿透
2、緩存雪崩
3、緩存擊穿
4、最佳實踐
一、系統概述
緩存(Cache)是通過臨時存儲高頻訪問數據來加速數據訪問的技術組件,本質是"空間換時間"的典型實踐。其核心價值體現在:
- 性能加速:縮短數據訪問路徑(CPU緩存 vs 內存訪問速度差達100倍)
- 資源節約:減少對底層數據源的訪問壓力(數據庫查詢成本降低80%+)
- 系統穩定:應對突發流量沖擊(如秒殺場景QPS可達10萬級)
二、名詞解釋
- 緩存命中率:請求緩存時,數據存在的概率(命中次數 / 總請求次數),命中率>90%為高效,<70%需優化(如調整淘汰策略或容量);
- 緩存穿透:請求不存在的數據(如惡意攻擊或無效ID),繞過緩存直擊后端;
- 緩存擊穿:熱點數據過期瞬間,高并發請求壓垮數據庫;
- 緩存雪崩:大量緩存同時過期,導致請求洪峰沖擊后端;
- 緩存污染:緩存中存儲了大量非高頻訪問或無效數據,導致緩存命中率下降,進而降低系統整體性能的現象。本質是緩存資源被低價值數據占用,無法有效服務高頻請求。
- TTL(Time To Live):緩存數據存活時間;
- 冷熱數據分離:高頻/低頻數據分區存儲
- 緩存預熱:系統啟動時加載熱點數據
三、淘汰策略
緩存系統通過淘汰策略在容量不足時決定移除哪些數據,核心目標是最大化緩存命中率。
策略 | 時間復雜度 | 空間開銷 | 適用場景 | 命中率 |
LRU | O(1) | 中 | 通用場景(Web緩存) | ★★★★☆ |
LFU | O(1)~O(log n) | 高 | 熱點數據集中(視頻推薦) | ★★★★★ |
FIFO | O(1) | 低 | 簡單順序訪問(日志緩沖) | ★★☆☆☆ |
TTL | O(log n) | 中 | 時效性數據(會話/驗證碼) | ★★★☆☆ |
Random | O(1) | 低 | 低成本容忍場景 | ★★☆☆☆ |
1、LRU
核心思想:優先淘汰最久未被訪問的數據
數據結構:哈希表 + 雙向鏈表(哈希表存儲鍵值對,鏈表維護訪問順序)
操作流程:
- 數據訪問:
- 命中緩存:將節點移到鏈表頭部
- 未命中:從數據庫加載,新節點插入鏈表頭部
- 淘汰觸發:
- 鏈表尾部節點(最久未訪問)被移除
- 同步刪除哈希表中對應鍵
優點:
- 高效反映時間局部性(最近訪問的數據更可能再被訪問)
- 時間復雜度:O(1)(哈希表定位 + 鏈表移動)
缺點:
- 突發批量訪問可能污染緩存(如全表掃描)
- 鏈表維護增加內存開銷
2、LFU
核心思想:優先淘汰訪問頻率最低的數據
數據結構:雙哈希表(鍵值存儲 + 頻率-鍵列表) + 最小堆/雙向鏈表
操作流程:
- 數據訪問:
- 命中緩存:增加計數,調整在頻率鏈表中的位置
- 淘汰觸發:
- 移除最低頻率鏈表中的最早節點(LRU作為次級策略)
- 更新最小頻率值
優點:
- 精準保護高頻訪問數據
- 適合長期熱點數據場景(如電商熱門商品)
缺點:
- 新數據易被淘汰(初始頻率低)
- 維護成本高(需頻率排序)
- 歷史高頻但不再訪問的數據可能滯留
3、FIFO
核心思想:按進入緩存的順序淘汰
數據結構:隊列(數組/鏈表實現)
操作流程:
(1)數據寫入:新數據加入隊尾
(2)淘汰觸發:移除隊首數據
優點:
- 實現簡單(僅需隊列)
- 零額外內存開銷
缺點:
- 忽略訪問模式(可能淘汰熱點數據)
- 緩存命中率通常最低
4、TTL
核心思想:基于過期時間自動淘汰
數據結構:哈希表 + 時間堆(或輪詢檢查)
操作流程:
- 數據寫入:設置過期時間戳(當前時間 + TTL)
- 淘汰觸發:
- 主動:定期掃描過期數據(定時器)
- 被動:訪問時檢查過期并刪除
優點:
- 保證數據時效性(適合會話緩存)
- 避免手動清理
缺點:
- 可能提前移除仍有價值的數據
- 掃描機制消耗CPU(大緩存需優化)
5、Random
核心思想:隨機選擇數據淘汰
數據結構:動態數組(如Python list)
操作流程:
- 淘汰觸發:隨機選擇鍵刪除
- 數據維護:數組動態調整
優點:
- 實現極其簡單
- 無狀態維護成本
缺點:
- 可能誤刪高頻數據
- 性能波動不可預測
四、讀寫模式
主要是要保證數據的一致性。
1、Cache Aside(旁路緩存)
旁路緩存模式,也稱為懶加載(Lazy Loading),是最常見的緩存模式。
應用程序直接與緩存和數據庫(或主數據存儲)交互。
優點:實現簡單,緩存只保存實際被請求的數據,節省內存。
缺點:緩存未命中時,需要訪問數據庫,可能導致延遲。另外,在寫操作后立即讀,可能會因為緩存失效而讀到舊數據(需要等到下次加載),但通常通過刪除緩存保證一致性。
讀流程
- 緩存命中:應用程序首先檢查緩存。如果數據存在(命中),則直接返回緩存數據。
- 緩存未命中:
????????(1)應用程序從數據庫中讀取數據。
? ? ? ? (2)將讀取到的數據寫入緩存(以便后續讀取命中)。
? ? ? ? (3)返回數據。
寫流程
- 應用程序直接更新數據庫。
- 同時,使緩存中對應的數據失效(刪除緩存項)。這樣,下次讀取時,會觸發緩存未命中,從而從數據庫加載最新數據并重新填充緩存。
?
2、Write Through(直寫)
在直寫模式中,緩存作為數據庫的代理層。
寫操作總是先經過緩存,然后由緩存同步更新到數據庫。
優點:緩存和數據庫始終保持一致(強一致性)。讀操作很少會訪問數據庫,因為寫操作已經更新了緩存。
缺點:寫操作延遲較高,因為需要等待數據庫寫入完成。如果數據不經常被讀取,那么寫入緩存可能造成資源浪費(因為每次寫都更新緩存,即使很少讀)。
讀流程
- 緩存命中:直接返回緩存數據。
- 緩存未命中:
? ? ? ? (1)緩存從數據庫中加載數據(或由應用程序觸發加載)。
? ? ? ? (2)將數據放入緩存。
? ? ? ? (3)返回數據。
寫流程
(1)應用程序更新緩存(如果數據在緩存中不存在,則創建緩存項)。
(2)緩存立即將數據同步寫入數據庫(通常在一個事務內)。
(3)只有在數據庫寫入成功后,寫操作才算完成。
?
3、Write Back(回寫)
回寫模式(也稱為Write-Behind)中,寫操作首先寫入緩存,然后異步批量寫入數據庫。緩存作為寫操作的緩沖區。
優點:寫操作非常快,因為應用程序不需要等待數據庫寫入。可以合并多次寫操作,減少數據庫壓力。
缺點:數據不一致的風險(緩存和數據庫在異步同步前不一致)。如果緩存崩潰,尚未寫入數據庫的數據會丟失。因此,通常需要額外的機制(如寫日志)來保證數據持久性。
讀流程
- 緩存命中:直接返回緩存數據。
- 緩存未命中:
? ? ? ? (1)從數據庫中加載數據到緩存。
? ? ? ? (2)返回數據。
寫流程
(1)應用程序更新緩存(如果數據不在緩存中,則先加載到緩存再更新,或者直接創建新的緩存項)。
(2)緩存標記數據為“臟”(dirty),表示需要同步到數據庫。
(3)緩存立即返回成功給應用程序(無需等待數據庫寫入)。
(4)緩存會在之后的某個時間點(例如,緩存滿時、定時任務、或者低負載時)將“臟”數據批量寫入數據庫。
?
五、問題方案
1、緩存穿透
問題原因
????????當查詢不存在的數據時(如無效ID、不存在的用戶名),每次請求都會穿透緩存層直接訪問數據庫。在惡意攻擊場景下(如腳本批量請求隨機ID),數據庫會持續承受無效查詢壓力,導致性能急劇下降甚至崩潰。這種現象與正常緩存未命中的區別在于:正常未命中是偶發的,而穿透是持續性的無效查詢。
// 大量惡意調用
getData("invalid_id_1");
getData("invalid_id_2");
...// 緩存訪問接口
std::string getData(const std::string& key)
{auto data = cache.get(key); // 緩存查詢if (data.empty()) {data = db.query(key); // 緩存未命中,查詢數據庫cache.set(key, data); // 寫入緩存}return data;
}
解決方案
(1)布隆過濾器(Bloom Filter)
- 在緩存層前設置布隆過濾器作為屏障
- 工作原理:使用多個哈希函數將鍵映射到位數組中,查詢時:
- 若鍵不在過濾器中 → 直接返回空(攔截非法請求)
- 若鍵可能存在 → 繼續查詢緩存/數據庫
- 特點:存在誤判率(通常<1%),但內存效率極高(1億鍵僅需約100MB)
(2)空值緩存(Cache Null Object)
- 對查詢結果為空的鍵,緩存特殊標記(如"NULL")
- 設置較短過期時間(5-30秒),防止惡意請求耗盡空間
- 需配合監控清理機制,避免存儲過多無效鍵
// C++ 布隆過濾器+空值緩存實現
class BloomFilter {
private:std::vector<bool> bits;std::vector<std::hash<std::string>> hashers;public:BloomFilter(size_t size, int hash_count) : bits(size), hashers(hash_count) {}void add(const std::string& key) {for (auto& hash_fn : hashers) {size_t pos = hash_fn(key) % bits.size();bits[pos] = true;}}bool may_contain(const std::string& key) {for (auto& hash_fn : hashers) {size_t pos = hash_fn(key) % bits.size();if (!bits[pos]) return false;}return true;}
};// 使用示例
BloomFilter filter(1000000, 3); // 100萬位,3個哈希std::string get_data(const std::string& key) {// 布隆過濾器攔截if (!filter.may_contain(key)) return "";// 緩存查詢auto data = cache.get(key);if (data == "NULL") return ""; // 空值標識if (data.empty()) {data = db.query(key);if (data.empty()) {cache.set(key, "NULL", 15); // 緩存空值15秒return "";}cache.set(key, data, 3600); // 緩存有效數據}return data;
}
2、緩存雪崩
問題原因
????????當大量緩存在同一時間段集中過期(如緩存初始化時設置相同TTL),瞬時會有海量請求穿透到數據庫。典型場景包括:
- 系統啟動時批量加載緩存
- 定時任務刷新緩存
- 緩存服務器重啟
雪崩效應會導致數據庫出現流量尖峰,引發連鎖故障(如連接池耗盡、CPU過載)。
解決方案
(1)差異化過期時間
- 基礎過期時間 + 隨機偏移(如30分鐘 ± 5分鐘)
- 確保緩存失效時間均勻分布,避免集中失效
(2)雙層緩存策略
- 主緩存:設置較短TTL(30分鐘),承擔日常請求
- 備份緩存:設置長TTL(24-48小時)
- 當主緩存失效時:
- 先返回備份緩存數據
- 異步重建主緩存
- 保證即使主緩存失效,仍有備份數據可用
(3)熱數據永不過期
- 對核心熱數據(如首頁推薦)采用邏輯過期:
- 物理上永不過期
- 后臺線程定期更新數據
- 數據對象包含邏輯過期時間戳
// C++ 雙層緩存+隨機TTL實現
std::string get_data_avalanche_protected(const std::string& key) {// 隨機數生成器static thread_local std::mt19937 rng(std::random_device{}());static std::uniform_int_distribution<int> dist(-300, 300);// 優先查詢主緩存if (auto data = cache.get("primary_" + key); !data.empty()) return data;// 查詢備份緩存if (auto backup = cache.get("backup_" + key); !backup.empty()) {// 異步重建主緩存std::thread([key, backup] {int ttl = 1800 + dist(rng); // 30分鐘基礎+隨機偏移cache.set("primary_" + key, backup, ttl);}).detach();return backup;}// 查詢數據庫auto data = db.query(key);if (!data.empty()) {int primary_ttl = 1800 + dist(rng);cache.set("primary_" + key, data, primary_ttl);cache.set("backup_" + key, data, 86400); // 備份24小時}return data;
}
3、緩存擊穿
問題原因
當某個熱點Key突然失效時,瞬時海量并發請求同時涌入數據庫。與雪崩的區別在于:
- 雪崩:大量不同Key同時失效
- 擊穿:單個熱點Key失效引發風暴
核心危害:
- 單點數據庫壓力暴增(萬級QPS)
- 可能引發連接池耗盡
- 重建緩存時的重復查詢浪費資源
解決方案
(1)互斥鎖重建(分布式鎖)
- 當緩存失效時,僅允許一個線程執行數據庫查詢
- 其他線程阻塞等待或重試
- 關鍵點:
- 鎖粒度:Key級別鎖而非全局鎖
- 鎖超時:防止死鎖(通常5-10秒)
- 雙重檢查:獲取鎖后再次驗證緩存
(2)邏輯過期永不過期
- 緩存永不物理刪除
- 數據結構包含邏輯過期時間戳
- 請求處理流程:
- 返回當前緩存數據(無論是否過期)
- 異步檢查過期狀態
- 過期則觸發重建
- 優點:零等待時間,保證高并發下的可用性
(3)熱點數據監控+預加載
- 實時監控Key訪問頻率
- 識別熱點Key后:
- 延長其TTL
- 提前異步刷新
- 在多個緩存節點復制
// C++ 互斥鎖+邏輯過期實現
class HotspotProtection {
private:std::shared_mutex global_mutex;std::unordered_map<std::string, std::shared_ptr<std::mutex>> key_mutexes;struct CacheItem {int64_t logical_expire; // 邏輯過期時間戳(毫秒)std::string data;};public:std::string get_data(const std::string& key) {// 獲取當前緩存項auto item = cache.get<CacheItem>(key);// 未邏輯過期直接返回if (item.logical_expire > get_system_time_millis()) return item.data;// 獲取Key級別鎖std::shared_ptr<std::mutex> key_mutex;{std::shared_lock read_lock(global_mutex);auto it = key_mutexes.find(key);if (it != key_mutexes.end()) key_mutex = it->second;}if (!key_mutex) {std::unique_lock write_lock(global_mutex);key_mutex = key_mutexes[key] = std::make_shared<std::mutex>();}// 鎖定并重建std::unique_lock lock(*key_mutex);// 雙重檢查item = cache.get<CacheItem>(key);if (item.logical_expire > get_system_time_millis()) return item.data;// 查詢數據庫auto new_data = db.query(key);int64_t new_expire = get_system_time_millis() + 3600000; // 1小時后過期// 更新緩存cache.set(key, CacheItem{new_expire, new_data});return new_data;}
};
4、最佳實踐
防御策略 | 適用場景 | 優點 | 缺點 |
布隆過濾器 | 防惡意請求/無效鍵 | 內存高效,攔截精確 | 存在誤判率 |
空值緩存 | 處理不存在數據 | 簡單易實現 | 可能存儲大量無效鍵 |
隨機TTL | 防批量緩存同時失效 | 有效分散壓力 | 無法應對熱點Key失效 |
雙層緩存 | 高可用場景 | 主備切換平滑 | 增加內存開銷 |
互斥鎖重建 | 防熱點Key擊穿 | 保證數據一致性 | 增加請求延遲 |
邏輯過期 | 超高并發熱點數據 | 零等待時間,極致性能 | 實現復雜度高 |
分層防御體系
- 第一層:布隆過濾器攔截非法請求
- 第二層:空值緩存處理無效查詢
- 第三層:隨機TTL+雙層緩存防雪崩
- 第四層:互斥鎖+邏輯過期防擊穿
熱點數據特殊處理
設置熱點閾值,對于熱點 key,延長 TTL、多節點復制
// 熱點Key識別與預加載
class HotspotManager {
public:void monitor_access(const std::string& key) {access_count[key]++;if (access_count[key] > 1000) { // 達到熱點閾值extend_ttl(key, 3600); // 延長TTLpreload_replica(key); // 多節點復制}}
};
熔斷降級機制
- 當數據庫壓力超過閾值時:
- 自動觸發熔斷
- 返回降級內容(如默認推薦)
- 記錄日志異步補償
持續監控指標
緩存命中率 > 95% # 低于閾值告警
數據庫QPS < 3000 # 超過閾值擴容
穿透請求量 < 100/s # 超過閾值啟動防御