30分鐘自學教程:Redis緩存穿透原理與解決方案
目標
- 理解緩存穿透的成因及危害。
- 掌握布隆過濾器、空值緩存等核心防御技術。
- 能夠通過代碼實現請求攔截與緩存保護。
- 學會限流降級、異步加載等應急方案。
教程內容
0~2分鐘:緩存穿透的定義與核心原因
- 定義:惡意或異常請求頻繁訪問數據庫中不存在的數據,繞過緩存直接沖擊數據庫。
- 典型場景:
- 攻擊者偽造大量非法ID(如負數、超長字符串)。
- 業務未對查詢參數校驗,或未緩存空結果。
- 危害:
- 數據庫壓力激增,甚至宕機。
- 正常服務被惡意請求拖垮。
2~5分鐘:代碼模擬穿透場景(Java示例)
// 未做防護的查詢方法(模擬穿透問題)
public Product getProduct(String id) { String key = "product:" + id; Product product = redisTemplate.opsForValue().get(key); if (product == null) { // 直接查詢數據庫(未緩存空值) product = productService.loadFromDB(id); if (product != null) { redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS); } } return product; // 惡意請求會反復查詢數據庫
}
驗證問題:
- 使用JMeter發送100次
id=-1
的請求,觀察數據庫查詢次數是否為100次(穿透發生)。
5~12分鐘:解決方案1——布隆過濾器(Bloom Filter)
- 原理:基于位數組和哈希函數,快速判斷數據是否可能存在于數據庫,攔截非法請求。
- 代碼實現(Redisson布隆過濾器):
// 初始化布隆過濾器并預熱合法數據
public class BloomFilterInit { private RBloomFilter<String> bloomFilter; @PostConstruct public void init() { bloomFilter = redissonClient.getBloomFilter("product_bloom"); bloomFilter.tryInit(100000L, 0.01); // 容量10萬,誤判率1% List<String> validIds = productService.getAllValidIds(); validIds.forEach(bloomFilter::add); }
} // 查詢時攔截非法請求
public Product getProductWithBloomFilter(String id) { if (!bloomFilter.contains(id)) { return null; // 直接攔截 } // 正常查詢邏輯...
}
- 注意事項:
- 誤判率需根據業務容忍度調整(如0.1%更嚴格,但占用更多內存)。
- 需定期同步布隆過濾器與數據庫的合法數據(如定時任務)。
12~20分鐘:解決方案2——空值緩存(Cache Null)
- 原理:即使數據庫不存在該數據,也緩存空值(如“NULL”),避免重復穿透。
- 代碼實現:
public Product getProductWithNullCache(String id) { String key = "product:" + id; Product product = redisTemplate.opsForValue().get(key); if (product == null) { product = productService.loadFromDB(id); if (product == null) { // 緩存空值,5分鐘過期 redisTemplate.opsForValue().set(key, "NULL", 5, TimeUnit.MINUTES); return null; } redisTemplate.opsForValue().set(key, product, 1, TimeUnit.HOURS); } else if ("NULL".equals(product)) { return null; // 直接返回空結果 } return product;
}
- 優化點:
- 空值過期時間不宜過長(避免存儲大量無效Key)。
- 可結合布隆過濾器,減少空值緩存的數量。
20~25分鐘:解決方案3——請求參數校驗
- 原理:在業務層攔截非法參數(如非數字ID、越界值)。
- 代碼實現(Spring Boot參數校驗):
public Product getProduct(@PathVariable String id) { // 校驗ID格式(僅允許數字) if (!id.matches("\\d+")) { throw new IllegalArgumentException("非法ID格式"); } // 校驗ID范圍(如大于0) long numericId = Long.parseLong(id); if (numericId <= 0) { throw new IllegalArgumentException("ID必須為正數"); } // 正常查詢邏輯...
}
- 擴展:
- 使用Hibernate Validator實現注解式校驗(如
@Min(1)
)。
- 使用Hibernate Validator實現注解式校驗(如
25~28分鐘:應急處理方案
- 限流降級(Guava RateLimiter):
private RateLimiter rateLimiter = RateLimiter.create(100); // 每秒100個請求 public Product getProduct(String id) { if (!rateLimiter.tryAcquire()) { throw new RuntimeException("請求過于頻繁,請稍后重試"); } // 正常查詢邏輯...
}
- 異步加載(CompletableFuture):
public Product getProductAsync(String id) { String key = "product:" + id; Product product = redisTemplate.opsForValue().get(key); if (product == null) { CompletableFuture.runAsync(() -> { Product dbProduct = productService.loadFromDB(id); if (dbProduct != null) { redisTemplate.opsForValue().set(key, dbProduct, 1, TimeUnit.HOURS); } }); } return product; // 可能返回空,但避免阻塞請求
}
28~30分鐘:總結與優化方向
- 核心原則:攔截非法請求、緩存空值、業務兜底。
- 高級優化:
- 結合Redis Module的
RedisBloom
擴展(生產級布隆過濾器)。 - 動態調整限流閾值(如根據數據庫負載自動限流)。
- 結合Redis Module的
練習與拓展
練習
- 實現一個布隆過濾器,攔截
id<=0
的非法請求。 - 修改空值緩存邏輯,動態設置隨機過期時間(如5~15分鐘)。
推薦拓展
- 學習
RedisBloom
模塊的安裝與使用。 - 研究分布式限流框架(如Sentinel)的實現原理。
- 探索緩存穿透與緩存擊穿的綜合防護方案。