# Redis緩存穿透怎么理解
## 引言
在當今互聯網應用中,緩存技術已成為提升系統性能的關鍵組件。作為高性能鍵值存儲系統的代表,Redis被廣泛應用于緩存場景中。然而,在使用Redis緩存時,開發者常會遇到"緩存穿透"這一棘手問題。本文將深入剖析緩存穿透的概念、產生原因、危害以及多種解決方案,幫助開發者構建更健壯的緩存系統。
## 一、緩存穿透的基本概念
### 1.1 什么是緩存穿透
緩存穿透(Cache Penetration)是指**查詢一個根本不存在的數據**,導致這個查詢請求直接穿過緩存層,每次都要訪問持久化存儲(如數據庫)的現象。與緩存擊穿、緩存雪崩不同,穿透問題關注的是"不存在數據"的異常訪問場景。
### 1.2 相關術語辨析
- **緩存擊穿**:熱點key過期瞬間大量請求直達數據庫
- **緩存雪崩**:大量key同時過期導致請求暴擊存儲層
- **緩存穿透**:查詢不存在數據的持續高壓請求
三者對比表:
| 問題類型 | 觸發條件 | 影響范圍 | 典型場景 |
|---------|----------|----------|----------|
| 穿透 | 查詢不存在數據 | 單個或多個不存在key | 惡意攻擊、業務bug |
| 擊穿 | 熱點key過期 | 單個熱點key | 秒殺商品查詢 |
| 雪崩 | 大量key同時過期 | 大批量key | 緩存初始化、定時任務刷新 |
## 二、緩存穿透的產生原因
### 2.1 惡意攻擊場景
攻擊者構造大量數據庫不存在的key進行請求,例如:
- 遍歷不存在的用戶ID:`user:9999999`
- 使用負數值或超長字符串:`product:-10086`
- 隨機生成UUID作為查詢參數
### 2.2 業務邏輯缺陷
- 未校驗的輸入參數直接作為緩存key
- 誤刪除數據后未清理緩存
- 分頁查詢未處理越界請求
### 2.3 數據同步延遲
新業務上線時:
1. 用戶查詢剛下架的商品
2. 緩存已刪除但搜索引擎仍有索引
3. 持續產生對不存在商品的查詢
## 三、緩存穿透的危害分析
### 3.1 對數據庫的直接壓力
典型案例:
- 某電商平臺遭遇CC攻擊,攻擊者每秒發送2萬次不存在的商品ID查詢
- Redis未命中導致QPS全部壓到MySQL
- 數據庫CPU飆升至90%,正常業務查詢響應時間從50ms升至2s+
### 3.2 系統資源浪費
資源消耗對比表:
| 資源類型 | 正常查詢 | 穿透查詢 |
|---------|----------|----------|
| Redis連接 | 1次 | 1次 |
| 網絡IO | 緩存返回約1KB | 完整查詢流程 |
| CPU周期 | 緩存解碼納秒級 | SQL解析+執行毫秒級 |
### 3.3 連帶效應
- 連接池被占滿導致正常請求阻塞
- 磁盤IO升高影響其他業務表查詢
- 可能觸發數據庫的慢查詢告警機制
## 四、解決方案全景圖
### 4.1 防御矩陣
┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │ 客戶端防護 │───?│ 緩存層防護 │───?│ 存儲層防護 │ └───────────────┘ └───────────────┘ └───────────────┘
### 4.2 方案選型參考
根據QPS級別選擇策略:
- 萬級QPS:布隆過濾器+空值緩存
- 十萬級QPS:布隆過濾器+限流
- 百萬級QPS:多級緩存+彈性擴縮容
## 五、詳細解決方案
### 5.1 空對象緩存(Null Caching)
實現示例:
```java
public Product getProduct(String id) {
// 嘗試從緩存獲取
Product product = redis.get("product:" + id);
if (product != null) {
return product instanceof NullProduct ? null : product;
}
// 查詢數據庫
product = db.query("SELECT * FROM products WHERE id = ?", id);
// 數據庫不存在則緩存空對象
if (product == null) {
redis.setex("product:" + id, 300, new NullProduct());
return null;
}
// 正常緩存數據
redis.setex("product:" + id, 3600, product);
return product;
}
注意事項:
- 設置較短的TTL(如5-10分鐘)
- 空對象應盡量?。≧edis的""或特定標記對象)
- 需考慮緩存污染問題
布隆過濾器位數組操作流程:
1. 初始化m位的bit數組,全部置0
2. 添加元素時,用k個hash函數計算得到k個位置并置1
3. 檢查元素時,若所有hash位置都為1則可能存在
# 使用Redis的位圖實現
import redis
from hashlib import md5
class RedisBloomFilter:
def __init__(self, key, expected_insertions=1000000, fpp=0.01):
self.key = key
self.redis = redis.StrictRedis()
# 計算最優參數
self.size = self._optimal_size(expected_insertions, fpp)
self.hash_count = self._optimal_hash_count(expected_insertions, self.size)
def add(self, item):
for seed in range(self.hash_count):
index = self._hash(item, seed) % self.size
self.redis.setbit(self.key, index, 1)
def exists(self, item):
for seed in range(self.hash_count):
index = self._hash(item, seed) % self.size
if not self.redis.getbit(self.key, index):
return False
return True
// Express中間件示例
app.use('/api/products/:id', (req, res, next) => {
const id = req.params.id;
// 格式校驗
if (!/^\d{1,8}$/.test(id)) {
return res.status(400).json({error: 'Invalid ID format'});
}
// 范圍校驗
const numId = parseInt(id);
if (numId < 1 || numId > MAX_PRODUCT_ID) {
return res.status(404).json({error: 'Product not found'});
}
next();
});
-- token_bucket.lua
local key = KEYS[1]
local capacity = tonumber(ARGV[1])
local rate = tonumber(ARGV[2])
local now = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])
local last_tokens = tonumber(redis.call("hget", key, "tokens")) or capacity
local last_refreshed = tonumber(redis.call("hget", key, "last_refreshed")) or now
local delta = math.max(0, now - last_refreshed)
local new_tokens = math.min(capacity, last_tokens + delta * rate)
local allowed = new_tokens >= requested
local result = 0
if allowed then
result = 1
new_tokens = new_tokens - requested
end
redis.call("hset", key, "tokens", new_tokens)
redis.call("hset", key, "last_refreshed", now)
redis.call("expire", key, math.ceil(capacity / rate) * 2)
return result
Hystrix配置示例:
@HystrixCommand(
fallbackMethod = "getProductFallback",
commandProperties = {
@HystrixProperty(name="circuitBreaker.requestVolumeThreshold", value="20"),
@HystrixProperty(name="circuitBreaker.errorThresholdPercentage", value="50"),
@HystrixProperty(name="circuitBreaker.sleepWindowInMilliseconds", value="5000")
}
)
public Product getProduct(String id) {
// 業務邏輯
}
實時監控方案:
# 使用Redis的HyperLogLog統計key訪問
def track_key_access(key):
redis.pfadd("access_log", key)
# 每分鐘分析熱點
if time.time() % 60 == 0:
hot_keys = analyze_hot_keys()
update_bloom_filter(hot_keys)
def analyze_hot_keys():
all_keys = redis.pfcount("access_log")
return redis.execute_command("TOPK.LIST", "hot_keys")
// Spring EventListener示例
@EventListener
public void handleProductUpdate(ProductUpdateEvent event) {
CompletableFuture.runAsync(() -> {
// 預熱布隆過濾器
bloomFilter.add(event.getProductId());
// 加載二級緩存
loadingCache.put(event.getProductId(),
productService.getProduct(event.getProductId()));
}, warmUpExecutor);
}
典型架構示例:
客戶端 → CDN邊緣緩存 → L1 Redis → L2 Redis → 數據庫
↓
布隆過濾器層
問題現象: - 用戶搜索不存在的用戶名導致MySQL負載飆升 - 每秒約8000次穿透查詢
解決方案: 1. 部署Redis布隆過濾器集群 2. 使用用戶ID范圍分片(0-1億、1-2億…) 3. 添加名字格式校驗(長度2-20,僅允許特定字符)
效果: - 數據庫查詢下降99.8% - 布隆過濾器誤判率穩定在0.3%
挑戰: - 爬蟲遍歷商品ID - 商品下架后仍有大量查詢
實施步驟: 1. 商品下架時同步: - 刪除緩存 - 更新布隆過濾器 - 記錄到黑名單服務 2. 查詢鏈路:
graph TD
A[請求] --> B{布隆過濾器檢查}
B -->|存在可能| C[查詢緩存]
B -->|不存在| D[返回404]
C -->|命中| E[返回數據]
C -->|未命中| F[校驗黑名單]
F -->|在黑名單| D
F -->|不在| G[查詢數據庫]
Prometheus配置示例:
metrics:
cache_penetration:
type: counter
help: "Total cache penetration requests"
labels: [service]
bloom_filter_false_positives:
type: counter
help: "Bloom filter false positives"
db_fallback_queries:
type: gauge
help: "Current DB queries caused by cache miss"
# 基于突增比例的告警
def check_penetration_alert():
normal_rate = get_historical_penetration_rate()
current_rate = get_current_penetration_rate()
if current_rate > normal_rate * 5: # 5倍突增
send_alert("Cache penetration spike detected!")
if get_db_load() > 80: # 數據庫負載>80%
trigger_circuit_breaker()
緩存穿透問題猶如緩存系統的”免疫缺陷”,需要開發者構建多層次的防御體系。通過本文介紹的空對象緩存、布隆過濾器、請求校驗等組合策略,配合完善的監控機制,可以有效提升系統抗穿透能力。隨著技術的發展,新的解決方案將不斷涌現,但理解問題本質、根據業務特點設計針對性方案的原則永遠不會過時。
# 編譯redisbloom模塊
git clone https://github.com/RedisBloom/RedisBloom.git
cd RedisBloom
make
# 啟動Redis加載模塊
redis-server --loadmodule ./redisbloom.so
測試環境:4核8G云主機,Redis 6.2,MySQL 8.0
| 方案 | 吞吐量(QPS) | 平均延遲 | 數據庫負載 |
|---|---|---|---|
| 無防護 | 12,000 | 15ms | 90% |
| 空緩存 | 45,000 | 5ms | 30% |
| 布隆過濾器 | 78,000 | 2ms | % |
| 布隆+空緩存 | 65,000 | 3ms | % |
”`
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。