Redis缓存设计:缓存穿透、击穿、雪崩及解决方案
Redis缓存设计:缓存穿透、击穿、雪崩及解决方案
引言
在现代互联网架构中,缓存是应对高并发、提升系统响应速度的核心组件之一。作为最流行的KV存储中间件,Redis凭借其卓越的性能和丰富的数据结构,成为了缓存实现的首选。
然而,引入缓存并非“银弹”,它同时也引入了系统复杂度。在高并发场景下,如果缓存设计不当,不仅无法提升性能,反而可能导致数据库压力骤增,甚至引发服务宕机。我们常说的“缓存三大杀手”——缓存穿透、缓存击穿和缓存雪崩,就是开发者必须面对的经典难题。
本文将从原理出发,深入剖析这三种问题的成因,并结合Java代码实战,给出生产环境下的最佳解决方案。
核心概念辨析
在深入技术细节之前,我们需要清晰地界定这三个概念,这是解决问题的基础。
| 问题类型 | 核心现象 | 根本原因 | 后果 |
|---|---|---|---|
| 缓存穿透 | 查询一个根本不存在的数据 | 缓存和数据库都无数据,请求绕过缓存直接击打DB | 恶意攻击导致DB压力过大 |
| 缓存击穿 | 某个热点Key突然过期 | 高并发访问该Key,缓存失效瞬间流量直达DB | 瞬时DB负载激增 |
| 缓存雪崩 | 大量Key集中过期 | 同一时间大面积缓存失效,或Redis宕机 | DB瞬间承受巨大压力,系统崩溃 |
一、 缓存穿透
1.1 技术原理
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在。按照常规逻辑:查缓存 -> 缓存没有 -> 查数据库 -> 数据库也没有 -> 返回空。如果不做处理,每次请求都会穿透缓存,直接访问数据库。
场景假设:
黑客发起恶意攻击,构造了大量不存在的ID(如 -1 或超大数据ID)发起请求。由于缓存中没数据,这些请求全部涌向数据库,可能导致数据库IO/CPU飙升,甚至宕机。
1.2 解决方案
方案一:缓存空对象
当数据库查询为空时,依然将该结果(如空字符串或特定的Null值)写入缓存,并设置较短的过期时间。
- 优点:实现简单,维护方便。
- 缺点:
- 占用内存(可通过设置短TTL缓解)。
- 数据不一致窗口期:如果后续该Key对应的数据被写入了数据库,而缓存中还是空值,会导致数据暂时无法访问(需配合消息队列或主动清理)。
方案二:布隆过滤器
在访问缓存之前,先通过布隆过滤器判断Key是否可能存在。如果布隆过滤器说不存在,则该Key一定不存在,直接返回,无需查询缓存和数据库。
- 原理:布隆过滤器是一个很长的二进制向量和一系列随机映射函数。它利用位图存储数据特征,具有极高的空间效率和查询速度。
- 优点:内存占用极少,查询极快。
- 缺点:存在一定的误判率,但可以通过调整参数控制在可接受范围内。
1.3 实战代码(基于Redisson实现布隆过滤器)
以下代码展示了如何结合缓存空对象和布隆过滤器来防御穿透。
import org.redisson.api.RBloomFilter;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class ProductQueryService {
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Autowired
private RedissonClient redissonClient;
// 缓存空对象的统一标识
private static final String EMPTY_CACHE_VALUE = "NULL";
// 布隆过滤器名称
private static final String BLOOM_FILTER_NAME = "product_bloom_filter";
/**
* 初始化布隆过滤器(通常在系统启动时执行或数据写入时执行)
* 预期插入量100万,误判率0.01%
*/
public void initBloomFilter() {
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_NAME);
// 初始化布隆过滤器,预计元素100万,期望误判率为0.01%
bloomFilter.tryInit(1000000L, 0.0001);
// 模拟预热数据:将存在的商品ID加入布隆过滤器
bloomFilter.add("product:1001");
bloomFilter.add("product:1002");
}
/**
* 查询商品信息 - 防穿透方案
* @param key 商品Key
* @return 商品详情
*/
public String queryProduct(String key) {
// 1. 先查Redis缓存
String cacheValue = stringRedisTemplate.opsForValue().get(key);
if (cacheValue != null) {
// 如果命中缓存,需判断是否是空对象占位符
return EMPTY_CACHE_VALUE.equals(cacheValue) ? null : cacheValue;
}
// 2. 缓存未命中,使用布隆过滤器进行前置拦截
RBloomFilter<String> bloomFilter = redissonClient.getBloomFilter(BLOOM_FILTER_NAME);
// 如果布隆过滤器判断key不存在,则直接返回,避免查库
if (!bloomFilter.contains(key)) {
System.out.println("布隆过滤器拦截,Key不存在:" + key);
return null;
}
// 3. 布隆过滤器判断可能存在,查询数据库
// 模拟数据库查询
String dbValue = queryFromDatabase(key);
if (dbValue != null) {
// 4. 数据库存在,写入缓存
stringRedisTemplate.opsForValue().set(key, dbValue, 1, TimeUnit.HOURS);
return dbValue;
} else {
// 5. 数据库也不存在,缓存空对象,防止穿透
// 设置较短的过期时间,例如5分钟
stringRedisTemplate.opsForValue().set(key, EMPTY_CACHE_VALUE, 5, TimeUnit.MINUTES);
return null;
}
}
// 模拟数据库查询
private String queryFromDatabase(String key) {
System.out.println("查询数据库:" + key);
// 模拟数据库中只有1001和1002
if ("product:1001".equals(key)) return "iPhone 15 Pro";
if ("product:1002".equals(key)) return "MacBook Pro";
return null;
}
}
二、 缓存击穿
2.1 技术原理
缓存击穿针对的是热点Key。某个时刻,一个极度热门的Key(如秒杀商品、热门新闻)突然过期,此时海量并发请求瞬间击穿缓存,直接压垮数据库。
这与雪崩的区别在于:击穿是单个热点Key,雪崩是大量Key。虽然范围小,但热度高,破坏力同样巨大。
2.2 解决方案
方案一:互斥锁
当缓存失效时,只允许一个线程去查询数据库并重建

浙公网安备 33010602011771号