什么是缓存击穿、缓存穿透、缓存雪崩 ?
一、缓存击穿
定义:大量的请求同时查询一个热点key时,此时这个key正好失效,就会导致大量的请求打到数据库上
特征:
-
针对单个热点key
-
缓存刚好过期时发生
-
并发请求量大

方案一、互斥锁
加锁更新,比如请求查询A,发现缓存中没有,对A这个key加锁,同时去数据库查询数据,写入缓存,再返回给用户,这样后面的请求就可以从缓存中拿到数据了。


方案二
将过期时间组合写在value中,通过异步的方式不断的刷新过期时间,防止此类现象,不推荐使用
方案三、永不过期策略
-
对热点key设置永不过期
-
通过后台任务定期更新
个人不太推荐,因为可能因为某些活动的原因,每次的热点Key可能太一样,设置永久不过期可能会越来越占用redis内存
二、缓存穿透
定义:请求去查询一条记录,先查redis无,后查mysql无,都查询不到该条记录,但是请求每次都会打到数据库上面去,导致后台数据库压力暴增,这种现象我们称为缓存穿透,这个redis变成了一个摆设
特点:
-
查询不存在的数据
-
可能是恶意攻击
-
对数据库压力大

缓存穿透可能有两种原因:
-
自身业务代码问题
-
恶意攻击,爬虫造成空命中
它主要有两种解决办法:
1、缓存空值/默认值:一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库

缓存空值有两大问题:
1、缓存空污染(Cache Pollution)
缓存空值会导致缓存中存在大量的空值条目,这些条目占用了宝贵的缓存空间。随着时间的推移,缓存中的空值条目可能会越来越多,导致缓存的有效容量减少,影响其他有效数据的缓存命中率
示例
假设我们频繁查询不存在的用户ID,这些不存在的用户ID会被缓存为空值。缓存中的空值条目会逐渐增多,占用大量空间
public String getUserFromCache(String userId) {
String cacheValue = jedis.get(userId);
if (cacheValue != null) {
return "null".equals(cacheValue) ? null : cacheValue;
}
// 模拟从数据库查询数据
String dbValue = queryUserFromDatabase(userId);
if (dbValue != null) {
jedis.setex(userId, 300, dbValue); // 缓存有效数据
return dbValue;
} else {
jedis.setex(userId, 60, "null"); // 缓存空值
}
return null;
}
解决方案
设置较短的空值缓存时间:为空值设置一个较短的过期时间,确保空值不会长时间占用缓存空间。定期清理过期的空值条目
public String getUserFromCache(String userId) {
String cacheValue = jedis.get(userId);
if (cacheValue != null) {
return "null".equals(cacheValue) ? null : cacheValue;
}
// 模拟从数据库查询数据
String dbValue = queryUserFromDatabase(userId);
if (dbValue != null) {
jedis.setex(userId, 300, dbValue); // 缓存有效数据,设置较长的过期时间
return dbValue;
} else {
jedis.setex(userId, 10, "null"); // 缓存空值,设置较短的过期时间
}
return null;
}
2、缓存一致性问题(Cache Consistency Issues)
缓存空值可能会引入缓存一致性问题。如果数据库中原本不存在的数据在之后被添加,而缓存中仍然是空值,那么查询请求仍然会返回空值,而不是最新的数据。
示例
假设用户ID “123” 最初不存在于数据库中,所以我们缓存了一个空值。后来,用户ID “123” 被添加到数据库中,但由于缓存中仍然存在空值,查询请求仍然返回空值,而不是最新的用户数据。
解决方案
使用布隆过滤器:布隆过滤器可以用于快速判断一个元素是否可能存在。通过布隆过滤器,可以避免将空值缓存到缓存中,同时减少缓存污染的可能性
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;
public class CacheSolution {
private Jedis jedis;
private BloomFilter<String> bloomFilter;
public CacheSolution() {
jedis = new Jedis("localhost", 6379);
bloomFilter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 100000, 0.01);
}
public String getUserFromCache(String userId) {
if (!bloomFilter.mightContain(userId)) {
return null; // 如果布隆过滤器判断不存在,直接返回空值
}
String cacheValue = jedis.get(userId);
if (cacheValue != null) {
return "null".equals(cacheValue) ? null : cacheValue;
}
// 模拟从数据库查询数据
String dbValue = queryUserFromDatabase(userId);
if (dbValue != null) {
jedis.setex(userId, 300, dbValue); // 缓存有效数据
bloomFilter.put(userId); // 将键加入布隆过滤器
return dbValue;
} else {
// 不缓存空值
bloomFilter.put(userId); // 将键加入布隆过滤器
}
return null;
}
private String queryUserFromDatabase(String userId) {
// 模拟数据库查询逻辑
return "user_data_" + userId;
}
}
布隆过滤器里会保存数据是否存在,如果判断数据不不能再,就不会访问存储

能说说布隆过滤器吗 ?
布隆过滤器,它是一个连续的数据结构,每个存储位存储都是一个bit,即 0或者 1, 来标识数据是否存在。存储数据的时时候,使用K个不同的哈希函数将这个变量映射为bit列表的的K个点,把它们置为1。

我们判断缓存key是否存在,同样,K个哈希函数,映射到bit列表上的K个点,判断是不是 1:
1、如果全不是1,那么key不存在;
2、如果都是1,也只是表示key可能存在。
布隆过滤器也有一些缺点:
1、 它在判断元素是否在集合中时是有一定错误几率,因为哈希算法有一定的碰撞的概率。
2、不支持删除元素。
两种解决方案的对比:

3、在接口层增加校验,不合法的参数直接返回。不相信任务调用方,根据自己提供的 API 接口规范来,作为被调用方,要考虑可能任何的参数传值
三、缓存雪崩
定义:指大量缓存key同时失效 或 缓存服务宕机,导致所有请求直接到达数据库的现象
特征:
-
多个key同时失效
-
系统负载急剧上升
-
可能引发连锁故障

缓存雪崩是三大缓存问题里最严重的一种,我们来看看怎么预防和处理。
-
提高缓存可用性
-
1、集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
-
2、持久化机制
-
3、主从复制
-
-
过期时间
-
1、均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
-
2、热点数据永不过期。
-
-
熔断降级
-
1、服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
-
2、服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。
-

浙公网安备 33010602011771号