文章目录
- 1. 缓存穿透
- 2. 缓存击穿(失效)
- 3. 缓存雪崩
在高并发项目中,redis作为热门中间件,在为项目带来便利性的同时,也存在一些隐患,比如缓存穿透、缓存击穿、缓存雪崩等问题。这些问题的出现可能会使数据库遭受非常大的压力,以至于数据库崩溃!下面来介绍如何应对和解决这些问题!
1. 缓存穿透 缓存穿透是指查询一个根本不存在的数据,缓存层和数据库都不会命中,通常出于容错的考虑, 如果从数据库查不到数据则不写入缓存层,这样就导致每次查询时都会操作数据库。此时缓存对于数据库的保护作用将失去意义!
造成缓存穿透的原因有两个:
①:自身业务代码或数据出现问题
②:恶意***或者爬虫造成大量空命中!
缓存穿透解决方案如下:
1.1 缓存空对象
如果是同一时间多次获取某个不存在的数据,即使数据库返回null,也放入缓存,并设置短期过期时间。这样只有一次查询查数据库,在过期时间内其他的请求会在缓存层命中,不会打到数据库,可有效减少数据库压力!
但是对于同一时间多次获取多个不存在的数据时,如果也采用缓存空对象的方式,把所有不存在的key都放入redis,那么将消耗redis中不少的内存空间,这种的可以使用布隆过滤器来解决,要根据实际场景来决定策略!
1.2 布隆过滤器
对于恶意***,同一时间向服务器请求大量不存在的数据造成的缓存穿透,如果还使用缓存空对象的方式,将造成redis空间浪费。可以用布隆过滤器先做一次过滤,布隆过滤器存在于缓存层的上方,对于不存在的数据布隆过滤器一般都能够过滤掉,不让请求再往缓存层和数据库层发送。
当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
布隆过滤器由 一个大型的位数组 和 几个不一样的hash函数 组成
添加数据时:
当向布隆过滤器添加key时,会使用多个hash函数对key进行hash运算,得到一个整数值,然后拿这个整数与位数组进行取模运算(与hashMap数组寻址类似),得到这个key在数组中的位置,每一个hash函数都会计算得到一个位置,再把这几个位置都置为1就完成了add操作!
获取数据时:
向布隆过滤器询问 key 是否存在时,跟 add 一样,也会把 hash 的几个位置都算出来,看看位数组中这几个位置是否都为 1,只要有一个位为 0,那么说明布隆过滤器中这个key 不存在。如果都是 1,这并不能说明这个 key 就一定存在,只是极有可能存在。因为存在hash碰撞,这些位被置为 1 可能是因为其它的 key 存在所致!!如果这个位数组比较稀疏,这个概率就会很大,如果这个位数组比较拥挤,这个概率就会降低。
布隆过滤器优缺点:
布隆过滤器无法删除或者更新数据,所以布隆过滤器适用于数据相对稳定、实时性较低的应用场景,缺点是代码维护比较复杂,优点是内存空间占用少。比如恶意发送1000w个不存在的key,那么布隆过滤器中的位数组长度可能有1亿个位,这1亿个位占用100 000 000 ÷ 1024 ÷ 1024 ÷ 8 ≈ 12MB 的空间,但要把1000w个key全部空值缓存到redis中,内存占用绝对不止12MB!!
可以使用redisson实现布隆过滤器,引入依赖:<dependency>
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.6.5</version>
</dependency> 使用布隆过滤器需要进行初始化,把所有数据提前放入布隆过滤器,并且在增加数据时也要往布隆过滤器里放//构造Redisson客户端
Config config = new Config();
config.useSingleServer().setAddress("redis://localhost:6379");
RedissonClient redisson = Redisson.create(config);
//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
//根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L,0.03);
//把所有数据存入布隆过滤器
void init(){
for (String key: keys) {
bloomFilter.put(key);
}
} 布隆过滤器增加数据RBloomFilter<String> bloomFilter = redisson.getBloomFilter("nameList");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%,根据这两个参数会计算出底层的bit数组大小
bloomFilter.tryInit(100000000L,0.03);
//将aaa插入到布隆过滤器中
bloomFilter.add("aaa"); 获取数据时由 布隆过滤器 => 缓存 => 数据库 操作伪代码:布隆过滤器和空值缓存混合使用!String get(String key) {
// 从布隆过滤器这一级缓存判断下key是否存在
Boolean exist = bloomFilter.contains(key);
if(!exist){
return "";
}
// 从缓存中获取数据
String cacheValue = cache.get(key);
// 缓存为空
if (StringUtils.isBlank(cacheValue)) {
// 从存储中获取
String storageValue = storage.get(key);
cache.set(key, storageValue);
// 如果存储数据为空, 需要设置一个过期时间(300秒)
if (storageValue == null) {
cache.expire(key, 60 * 5);
}
return storageValue;
} else {
// 缓存非空
return cacheValue;
}
} 注意:布隆过滤器不能删除和更新数据,如果要删除或删除需要重新初始化布隆过滤器!
2. 缓存击穿(失效) 场景1:类似于京东的今日秒杀,秒杀开始日会批量上架部分商品到redis中,这些商品都带有过期时间 这个时间等于秒杀时间。在秒杀结束时,由于大批量缓存在同一时间失效可能导致大量请求同时穿透缓存直达数据库,可能会造成数据库瞬间压力过大甚至挂掉。
对于这种情况我们在批量增加缓存时最好将这一批数据的缓存过期时间设置为一个时间段内的不同时间。//设置一个过期时间(300到600之间的一个随机数)
int expireTime = new Random().nextInt(300) + 300; 场景2:高并发下查询单个过期数据或者冷门数据(比如疫情期间带货板蓝根!)。高并发情况下,某热点数据刚好过期,或者冷门数据没有往redis中存储。这样所有对这个数据的请求,都落到数据库,导致数据库压力过大崩溃。
解决:
- 加锁。使用setNX或者redisson加分布式锁。大量的并发只让一个线程去查数据库,查到以后存入缓存并释放锁,其他线程获取到锁,在先查询缓存时,就会查到第一个线程存入缓存的数据,不用查DB!
- 多级缓存。再套一层redis缓存,不同redis缓存的数据过期时间不一样!
3. 缓存雪崩 由于缓存层承载着大量请求, 有效地保护了存储层, 但是如果缓存层由于某些原因不能提供服务,比如超大并发过来,缓存层支撑不住宕掉后,数据库继续提供服务,这样流量会直接打到数据库,而数据库无法支持太高并发,导致数据库崩溃,最终导致整个系统崩溃。这种因为一个服务崩溃而蔓延导致整个系统崩溃叫做缓存雪崩!
解决:
- ①:保证redis高可用,使用redis集群架构,并分配足够节点,足以对抗高并发,避免redis宕机!
- ②:如果压力还是很大,可以使用限流或者熔断。使用压测工具预估redis集群并发极限,结合redis可以抗住的并发数,指定限流或熔断措施!被限流的请求给出友好提示。
比如服务降级,我们可以针对不同的数据采取不同的处理方式。当业务应用访问的是非核心数据(例如电商商品属性,用户信息等)时,暂时停止从缓存中查询这些数据,而是直接返回预定义的默认降级信息、空值或是错误提示信息;当业务应用访问的是核心数据(例如电商商品库存)时,仍然允许查询缓存,如果缓存缺失,也可以继续通过数据库读取。
- ③:如果压力很大,且场景支持异步,可以使用队列的方式来解决。把溢出的流量放在队列中,给出友好提示,提示前方有多少人排队!
- ④:加锁
|