## 什么是緩存穿透
緩存穿透是指**查詢一個根本不存在的數據,緩存層和存儲層都不會命中**,通常出于容錯的考慮,如果從存儲層查不到數據則不寫入緩存層,整個過程分為下圖3步:
* 1)緩存層不命中;
* 2)存儲層不命中,不將空結果寫回緩存;
* 3)返回空結果;

* **緩存穿透帶來的問題:**
* ①緩存穿透將導致不存在的數據每次請求都要到存儲層去查詢,**失去了緩存保護后端存儲的意義**;
* ②緩存穿透問題可能會**使后端存儲負載加大**,由于很多后端存儲不具備高并發性,甚至可能造成后端存儲宕掉。通常可以在程序中分別統計總調用數、緩存層命中數、存儲層命中數,如果發現大量存儲層空命中,可能就是出現了緩存穿透問題;
* **造成緩存穿透的基本原因有兩個:**
* 第一,自身業務代碼或者數據出現問題;
* 第二,一些惡意攻擊、爬蟲等造成大量空命中;
## 如何發現
1. 業務的響應時間;
2. 業務本身問題;
3. 相關指標:總調用數,緩存層命中數,存儲層命中數;
## 解決方法1--緩存空對象
* **概念**:當第2步存儲層不命中后,**仍然將空對象保留到緩存層中**,之后再訪問這個數據將會從緩存中獲取,這樣就保護了后端數據源;

**緩存空對象會有兩個問題:**
* 第一,空值做了緩存,意味著緩存層中存了更多的鍵,**需要更多的內存空間**(如果是攻擊,問題更嚴重),比較有效的方法是針對這類數據**設置一個較短的過期時間,讓其自動剔除**
* 第二,**緩存層和存儲層的數據會有一段時間窗口的不一致,可能會對業務有一定影響。**例如過期時間設置為5分鐘,如果此時存儲層添加了這個數據,那此段時間就會出現緩存層和存儲層數據的不一致,此時可以**利用消息系統或者其他方式清除掉緩存層中的空對象**
```
String get(String key) {
// 從緩存中獲取數據
String cacheValue = cache.get(key);
// 緩存為空
if (StringUtils.isBlank(cacheValue))
{
// 從存儲中獲取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存儲數據為空,需要設置一個過期時間(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
}
else {
//緩存非空
return cacheValue;
}
}
```
## 解決方法2--布隆過濾器
* 如下圖所示,在訪問緩存層和存儲層之前,**將存在的key用布隆過濾器提前保存起來,做第一層攔截**
* **例如**:一個推薦系統有4億個用戶id,每個小時算法工程師會根據每個用戶之前歷史行為計算出推薦數據放到存儲層中,但是最新的用戶由于沒有歷史行為,就會發生緩存穿透的行為,為此可以**將所有推薦數據的用戶**做成布隆過濾器。如果布隆過濾器**認為該用戶id不 存在,那么就不會訪問存儲層,在一定程度保護了存儲層**

* 這種方法**適用于數據命中不高、數據相對固定、實時性低(通常是數據集較大)的應用場景**,代碼維護較為復雜,但是緩存空間占用少
* **備注信息:**
* 關于布隆過濾器的介紹可以參閱:[https://blog.csdn.net/qq\_41453285/article/details/106416470](https://blog.csdn.net/qq_41453285/article/details/106416470)
* 可以參考:[https://en.wikipedia.org/wiki/Bloom\_filter](https://en.wikipedia.org/wiki/Bloom_filter)可以利用Redis的Bitmaps實現布隆過濾器,GitHub上已經開源了類似的方案,讀者可以進行參考:[https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter](https://github.com/erikdubbelboer/redis-lua-scaling-bloom-filter)
## # 兩種方案對比
* 前面介紹了緩存穿透問題的兩種解決方法(實際上這個問題是一個開放問題,有很多解決方法),下圖從適用場景和維護成本兩個方面對兩種方案進行分析

- Redis簡介
- 簡介
- 典型應用場景
- Redis安裝
- 安裝
- redis可執行文件說明
- 三種啟動方法
- Redis常用配置
- API的使用和理解
- 通用命令
- 數據結構和內部編碼
- 單線程
- 數據類型
- 字符串
- 哈希
- 列表
- 集合
- 有序集合
- Redis常用功能
- 慢查詢
- Pipline
- 發布訂閱
- Bitmap
- Hyperloglog
- GEO
- 持久化機制
- 概述
- snapshotting快照方式持久化
- append only file追加方式持久化AOF
- RDB和AOF的抉擇
- 開發運維常見問題
- fork操作
- 子進程外開銷
- AOF追加阻塞
- 單機多實例部署
- Redis復制原理和優化
- 什么是主從復制
- 主從復制配置
- 全量復制和部分復制
- 故障處理
- 開發運維常見問題
- Sentinel
- 主從復制高可用
- 架構說明
- 安裝配置
- 客戶端連接
- 實現原理
- 常見開發運維問題
- 高可用讀寫分離
- 故障轉移client怎么知道新的master地址
- 總結
- Sluster
- 呼喚集群
- 數據分布
- 搭建集群
- 集群通信
- 集群擴容
- 集群縮容
- 客戶端路由
- 故障轉移
- 故障發現
- 故障恢復
- 開發運維常見問題
- 緩存設計與優化
- 緩存收益和成本
- 緩存更新策略
- 緩存粒度控制
- 緩存穿透優化
- 緩存雪崩優化
- 無底洞問題優化
- 熱點key重建優化
- 總結
- 布隆過濾器
- 引出布隆過濾器
- 布隆過濾器基本原理
- 布隆過濾器誤差率
- 本地布隆過濾器
- Redis布隆過濾器
- 分布式布隆過濾器
- 開發規范
- 內存管理
- 開發運維常見坑
- 實戰
- 對文章進行投票
- 數據庫的概念
- 啟動多實例