Redis三大件 穿透、雪崩、击穿
一.缓存穿透:查无此物攻击。
问题本质
缓存穿透是指客户端发来的请求,在缓存和数据库中都无法找到,那么因此缓存就永远不会存在,所以每次请求都会被打到数据库
eg:黑客暴力扫描不存在的ID,然后发送大量垃圾请求,实现穿透攻击
解决方法
1.布隆过滤器
预加载所有可能存在的数据哈希值到布隆过滤器中,查询时先判断数据是否存在。
// 使用Guava实现布隆过滤器
public class BloomFilterDemo {
private static final int EXPECTED_INSERTIONS = 1000000; //预计插入数据量
private static final double FPP = 0.01; //允许的误判率(即允许 “可能存在但实际不存在” 的概率。)
private static BloomFilter<String> bloomFilter =
BloomFilter.create(Funnels.stringFunnel(StandardCharsets.UTF_8), EXPECTED_INSERTIONS, FPP);
// 初始化加载有效ID
@PostConstruct //标记方法在 Bean 初始化完成后自动执行
public void init() {
List<String> validIds = dao.getAllIds(); // 从数据库获取所有有效ID
validIds.forEach(bloomFilter::put); // 将所有ID写入布隆过滤器
}
public Object getData(String id) {
if (!bloomFilter.mightContain(id)) {
return "Invalid ID"; // 直接拦截非法请求
}
// 继续查询缓存和数据库...
}
}
2.缓存异常值
将查询结果为空的值也插入到缓存,并设置一个较短的过期时间。
public Object getDataWithNullCache(String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value != null) {
return "NULL".equals(value) ? null : value; // 空值标识处理
}
Object dbValue = db.get(key);
if (dbValue == null) {
redisTemplate.opsForValue().set(key, "NULL", 30, TimeUnit.SECONDS); // 空值缓存30秒
return null;
}
redisTemplate.opsForValue().set(key, dbValue, 5, TimeUnit.MINUTES);
return dbValue;
}
对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
布隆过滤器 | 内存高效,永久拦截非法请求 | 存在误判率,需要预加载数据 | 数据库定且可预加载 |
缓存空值 | 实现简单,实时生效 | 可能缓存大量无效Key | 动态变化的Key |
二.缓存雪崩:批量失效灾难
问题本质
- 批量过期:大量key在同一时间过期
- Redis宕机:集群故障导致所有请求被打到数据库
解决方法
随机过期时间
public void setCacheWithRandomExpire(String key, Object value) {
int baseExpire = 3600; // 基础过期时间1小时
int randomExpire = ThreadLocalRandom.current().nextInt(600); // 0-10分钟随机偏移
redisTemplate.opsForValue().set(
key,
value,
baseExpire + randomExpire,
TimeUnit.SECONDS
);
}
多级缓存架构
// 使用Caffeine作为本地缓存
public class MultiLevelCache {
private Cache<String, Object> localCache = Caffeine.newBuilder()
.expireAfterWrite(30, TimeUnit.SECONDS)
.maximumSize(10000)
.build();
public Object getData(String key) {
// 1.检查本地缓存
Object value = localCache.getIfPresent(key);
if (value != null) return value;
// 2.检查Redis缓存
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value); // 回填本地缓存
return value;
}
// 3.查询数据库并重建缓存...
}
}
三.缓存击穿:热点数据暴雷
问题本质
高并发访问的热点 key 突然失效,导致数据库被击穿
解决方法
分布式锁(Redisson实现)
当缓存失效时,通过分布式锁控制仅一个线程去数据库获取数据,重建缓存
public Object getDataWithLock(String key) {
Object value = redisTemplate.opsForValue().get(key);
if (value == null) {
RLock lock = redisson.getLock(key);
try {
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) { // 等待3秒,持有10秒
// 二次检查
value = redisTemplate.opsForValue().get(key);
if (value == null) {
value = db.get(key);
redisTemplate.opsForValue().set(key, value, 30, TimeUnit.MINUTES);
}
}
} finally {
lock.unlock();
}
}
return value;
}
逻辑过期时间
缓存永不过期,但存储数据时附加逻辑过期时间。
Redis中缓存永不过期(除非手动清理),由应用层逻辑过期时间戳来判断是否过期。
如果过期,则先使用Redis中的旧数据,然后使用异步单线程去数据库获取新数据,然后重新缓存到Redis。
@Data
public class LogicalExpireData {
private Object data;
private long expireTime; // 逻辑过期时间戳
}
public Object getDataWithLogicalExpire(String key) {
LogicalExpireData cached = redisTemplate.opsForValue().get(key);
if (cached == null) {
return loadDataAndSetCache(key); // 首次加载
}
if (System.currentTimeMillis() > cached.getExpireTime()) {
// 提交异步刷新任务
executor.submit(() -> {
if (getLock(key)) { // 获取互斥锁
try {
loadDataAndSetCache(key);
} finally {
releaseLock(key);
}
}
});
}
return cached.getData(); // 返回旧数据
}
private Object loadDataAndSetCache(String key) {
Object dbValue = db.get(key);
LogicalExpireData newData = new LogicalExpireData();
newData.setData(dbValue);
newData.setExpireTime(System.currentTimeMillis() + 3600_000); // 1小时后逻辑过期
redisTemplate.opsForValue().set(key, newData); //Redis中永不过期!
return dbValue;
}
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
分布式锁 | 数据强一致性 | 性能损耗较高 | 对一致性要求严格的场景 |
逻辑过期 | 零等待时间 | 存在短暂的数据不一致(新旧数据) | 高并发读场景 |
布隆过滤器是什么?
布隆过滤器是一种空间高效的概率型数据结构,用于快速判断一个元素是否属于某个集合
特点
- 可能误判(错误地认为某个不存在的元素存在):不同元素的哈希函数计算结果可能覆盖相同位,导致查询时误以为元素存在。
- 绝不漏判(错误地认为某个存在的元素不存在):如果元素实际存在于集合中,其哈希对应的所有位在添加时已被置为 1,不可能出现某一位为 0
数据结构
- 位数组:一个长度为 m 的二进制数组,初始所有位为 0。
- 哈希函数:使用 k 个不同的哈希函数,每个函数将元素映射到位数组的某个位置。
流程
(1)添加元素
元素"apple“经过k个不同的哈希函数计算后,得到k个哈希值h1(“apple”)、h2(“apple”)…hk(“apple”)
在位数组中将这k个哈希函数计算出来的位置设为1
// 示例:添加元素 "apple"
hash1("apple") → 位置3
hash2("apple") → 位置7
hash3("apple") → 位置12
将位数组的3、7、12位设置为1
(2)判断元素是否存在
将元素 y 通过同样的 k 个哈希函数计算,得到 k 个位置。
检查位数组中这 k 个位置是否全为 1:
- 全为 1 → 元素可能存在于集合(可能误判)
- 至少有一个 0 → 元素绝对不存在
// 示例:查询元素 "banana"
hash1("banana") → 位置3
hash2("banana") → 位置9
hash3("banana") → 位置12
检查位数组的3、9、12位:
- 位置9为0 → 元素不存在
误判率
误判率 p 与以下参数相关:
- m:位数组长度
- k:哈希函数数量
- n:集合中元素数量