Redis缓存穿透,缓存击穿,缓存雪崩分析
Redis缓存穿透,缓存击穿,缓存雪崩分析
1、简介:
Redis 作为一个典型的非关系型Nosql数据库,数据存储在内存中,读写速度非常快。使用Redis主要是防止大量的请求直接访问到数据库(Mysql等),产生大量的磁盘 I/O, 最终导致服务不可用。
特点: key-value键值类型存储系统、支持数据可靠存储(RDB、AOF持久化)、单进程单线程高性能服务器、单机qps(秒并发)可以达到10W
使用场景: 存储一些对数据的一致性要求不是特别高的数据、不经常改变的数据、但是经常性访问的数据。
当然使用Redis也不是能解决所有的问题,依然也存在弊端:
2、缓存穿透
每当请求从缓存中获取不到值时,请求就会到最底层的数据库(Mysql)中查找,如果从存储层查不到数据则不写入缓存,这将导致这个不存在的数据每次请求都要到存储层去查询,如果在高并发下大量的请求进入,从而可能压垮数据库(Mysql)。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
缓存穿透解决方案
有很多种方法可以有效地解决缓存穿透问题,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被 这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。另外也有一个更为简单粗暴的方法(我们采用的就是这种),如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。
方式二:缓存空值
//缓存这个空的值【伪代码】
public object GetProductList() {
int cacheTime = 30;
String cacheKey = "product_list";
cacheValue = CacheHelper.Get(cacheKey);
if (cacheValue != null) {
return cacheValue;
} else {
//数据库查询不到,为空
cacheValue = ProductDao.queryProducts;
if (cacheValue == null) {
//如果发现为空,设置个默认值,也缓存起来
cacheValue = string.Empty;
}
CacheHelper.Add(cacheKey, cacheValue, cacheTime);
return cacheValue;
}
}
3、缓存击穿
当我们使用Redis时,对于一些经常访问的数据,我们会放在Redis中,但为了保证数据尽量的一致性,我们会设置Key的过期时间,在高并发场景下,如果Key有效的情况下,Redis会帮我们抵挡很多的请求,防止请求直接打入底层数据库(Mysql), 但是如果刚好在高并发场景下,这个热点Key突然失效了,那么这时所有的请求就会进入数据库,可能我们还没来得及查询数据库并放入缓存,瞬间就把后面的DB压垮了。
缓存击穿解决方案:
使用互斥锁(mutex key)
业界比较常用的做法,是使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。加锁排队只是为了减轻数据库的压力,并没有提高系统吞吐量。假设在高并发下,缓存重建期间key是锁着的,这时过来1000个请求999个都在阻塞的。同样会导致用户等待超时,这是个治标不治本的方法。
public String get(key) {
String value = redis.get(key);
if (value == null) { //代表缓存值过期
//设置3min的超时,防止del操作失败的时候,下次缓存过期一直不能load db
if (redis.setnx(key_mutex, 1, 3 * 60) == 1) { //代表设置成功
value = db.get(key);
redis.set(key, value, expire_secs);
redis.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
4、缓存雪崩
当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
缓存雪崩 解决方案
大多数系统设计者考虑用加锁或者队列的方式保证来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落到底层存储系统上。还有一个简单方案就时讲缓存失效时间分散开,比如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
5、总结
- 缓存穿透:程序中没有缓存null值;当大量请求获取一个不存在的数据时,由于缓存中没有缓存到null值,大量请求直接访问数据库,数据库压力陡增,从而出现穿透问题!
- 解决方案:将查询结果为null的值缓存到redis中
- 缓存雪崩:大量缓存同一个时间内失效;
- 解决方案:在设置数据有效时间时,增加一个随机数
- 缓存击穿:大量请求同时访问同一个正好过期的缓存数据
- 解决方案:添加分布式锁
解决缓存一致性问题
常见的两种方式:
- 双写模式:修改数据完成后,直接修改缓存中的数据
- 失效模式:修改数据完成后,删掉缓存中的数据,等待下次主动查询进行更新
解决双写模式出现脏数据的问题:
- 给并发写操作加写锁
- 如果系统允许数据出现短暂的不一致,可忽略!等待数据过期自动删除,下次主动查询再缓存!