缓存穿透、击穿、雪崩的解决方案
一、缓存穿透(Cache Penetration)
定义:查询不存在的数据(如不存在的ID),绕过缓存直接穿透到数据库,导致数据库压力骤增。
解决方案:
-
布隆过滤器(Bloom Filter)
- 原理:通过位数组和哈希函数,快速判断数据是否可能存在。若布隆过滤器判定不存在,则直接拦截请求。
- 实战示例:电商商品查询,拦截非法ID请求。
- Java代码:
// 使用Guava实现布隆过滤器 BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000, 0.01); // 预热商品ID到布隆过滤器 productIds.forEach(bloomFilter::put);public Product getProduct(Long id) {if (!bloomFilter.mightContain(id)) {return null; // 直接拦截非法ID}// 后续查询缓存和数据库... }
-
缓存空值
- 原理:将查询结果为空的Key也缓存,避免重复穿透。
- 代码示例:
public Product getProduct(String key) {Product product = redis.get(key);if (product == null) {// 查询数据库product = db.get(key);if (product == null) {redis.set(key, "NULL", 60); // 缓存空值,设置短过期时间} else {redis.set(key, product, 3600);}}return product; }
二、缓存击穿(Cache Breakdown)
定义:热点Key突然失效,大量并发请求直接击穿到数据库。
解决方案:
-
互斥锁(Mutex Lock)
- 原理:使用Redis的SETNX命令实现分布式锁,仅允许一个线程重建缓存。
- 实战示例:秒杀活动中热门商品详情查询。
- Java代码:
public Product getProduct(String key) {Product product = redis.get(key);if (product == null) {String lockKey = "LOCK:" + key;if (redis.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS)) {try {product = db.get(key); // 查询数据库redis.set(key, product, 3600);} finally {redis.delete(lockKey);}} else {Thread.sleep(100); // 等待锁释放后重试return getProduct(key);}}return product; }
-
逻辑过期(永不过期策略)
- 原理:缓存不设置物理过期时间,后台异步更新数据。
- 实战示例:新闻热点实时更新。
- 代码示例:
public Product getProduct(String key) {Product product = redis.get(key);if (product.isExpired()) { // 检查逻辑过期时间// 异步线程更新缓存executor.submit(() -> {Product newProduct = db.get(key);redis.set(key, newProduct);});}return product; }
三、缓存雪崩(Cache Avalanche)
定义:大量Key同时失效,导致数据库瞬间压力过大。
解决方案:
-
随机过期时间
- 原理:为每个Key的TTL增加随机值,避免集中失效。
- 实战示例:首页推荐商品列表缓存。
- Java代码:
int baseTtl = 3600; int randomTtl = baseTtl + new Random().nextInt(600); // 增加0~10分钟随机值 redis.opsForValue().set(key, value, randomTtl, TimeUnit.SECONDS);
-
多级缓存(本地缓存+Redis)
- 原理:结合本地缓存(如Caffeine)和分布式缓存,降低雪崩风险。
- 代码示例:
// 本地缓存 Cache<Long, Product> localCache = Caffeine.newBuilder().expireAfterWrite(5, TimeUnit.MINUTES).build();public Product getProduct(Long id) {Product product = localCache.get(id, k -> redis.get(k));if (product == null) {product = db.get(id);redis.set(id, product, 3600);localCache.put(id, product);}return product; }
总结对比
问题类型 | 触发条件 | 核心解决方案 | 适用场景 |
---|---|---|---|
穿透 | 查询不存在的数据 | 布隆过滤器+空值缓存 | 防御恶意ID攻击 |
击穿 | 热点Key失效 | 互斥锁+逻辑过期 | 秒杀、热点新闻 |
雪崩 | 大量Key同时失效 | 随机TTL+多级缓存 | 大促活动首页数据 |
Tips:
- 布隆过滤器误判率:根据业务场景调整容量和哈希函数数量(Guava默认误判率0.01)[1]。
- 热点Key预热:在大促前手动加载热点数据到缓存,如电商双11商品预热[2]。