Redis 缓存穿透,雪崩
缓存穿透的原因及解决办法
缓存穿透是指查询一个不存在的数据,缓存层与存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层,比如下面的图中:
上图步骤为:
缓存层不命中
存储层不命中,不将空结果写回缓存
返回空结果
缓存穿透将导致不存在的数据每次请求都需要到存储层去查询,失去了缓存保护后端存储的意义。缓存穿透问题会可能使后端存储负载加大,由于很多存储不具备高并发性,甚至可能造成后端存储宕机,通常可以在程序中分别统计总调用数,缓存层命中数,存储层命中数,如果发现大量存储层命中为空,可能就是出现了缓存穿透问题。
出现的原因
自身的业务代码或者数据出现问题
一些恶意攻击,爬虫等造成大量空命中。
问题解决
缓存空对象
当第二步缓存不命中后,然要将空对象作为数据保留到缓存中,之后再来访问这个数据将会从缓存中进行获取,这样就保护的后端数据源,不会因为缓存穿透,导致大量请求到存储层,发生服务器宕机,或者拖累其他应用。
但是缓存空对象也会出现两个问题:
- 空值做了缓存
意味着缓存层中存了更多的键,需要更多的内存空间来进行存储(如果是属于恶意攻击,消耗会更大),比较有效防止的方式是针对这类数据设置一个较短的过期时间,让他自动删除。
- 缓存层与存储层的数据会出现一段时间的数据不一致,会对业务出现一定的影响
比如:此时我们对这个空数据设置了一个过期时间为5分钟,但是这个时候存储层添加了这条数据,那此段时间就会出现缓存层与存储层数据不一致的情况,针对这种情况我们可以使用消息系统或者其他方式消除缓存层存储的空对象。
下面是缓存空对象PHP伪代码:
<?php
public function get($id)
{
$key = "cache_".$id;
$ret = Redis::get($key);
if (empty($ret)) {
$ret = DB::table("article")->where("id",$id)->first();
Redis::set($key,$ret);
if (empty($ret)) {
Redis::expire($key,60*5); //设置过期时间五分钟
}
return $ret;
}else{
return $ret;
}
}
?>
使用布隆过滤器
在访问缓存层与存储层之前,将存在的key使用布隆过滤器提前保存起来,做一层拦截。Golang bitset 基本使用、布隆过滤器
缓存雪崩
由于缓存层承载着大量的请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。
缓存穿透,击穿,雪崩对比
缓存雪崩:当缓存服务器重启或者大量缓存集中在某一个时间段失效,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。
缓存击穿: key对应的数据存在,但在redis中过期,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮。
缓存穿透: key对应的数据在数据源并不存在,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。
雪崩预防方案
保证缓存层服务高可用性。和汽车一样都有多个轮胎一样,如果缓存层设计成高可用的,
即使个别节点,个别机器甚至是机房宕掉,依然可以提供服务,例如(Redis哨兵)Redis Sentinel和(Redis集群)Reis Cluster都实现了高可用。
后端限流并降级。限流是只允许一定时间内多少用户来进行访问,溢出的我们就直接打回,避免全部负载全部到到mysql服务器,降级是牺牲一些业务,提升mysql服务器请求处理容量。
过期时间错开。雪崩一般是指同一时刻出现大批量的数据过期导致请求全部打到mysql服务器,到服务器宕机,那么我们可以通过限制缓存的过期时间来进行预防,不同的key会去设置不同的过期时间。