合规国际互联网加速 OSASE为企业客户提供高速稳定SD-WAN国际加速解决方案。 广告
>[info] 缓存穿透的原因及解决办法 **什么是缓存穿透?** 缓存穿透是指查询一个不存在的数据,缓存层与存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层,比如下面的图中: ![](https://img.kancloud.cn/44/22/4422e394608f18aab8a94ac5e845f971_987x576.png) ***** **问题解决:** **1. 缓存空对象** 当第二步缓存不命中后,然要将空对象作为数据保留到缓存中,之后再来访问这个数据将会从缓存中进行获取,这样就保护的后端数据源,不会因为缓存穿透,导致大量请求到存储层,发生服务器宕机,或者拖累其他应用。 ![](https://img.kancloud.cn/de/d9/ded9c959afbfe0d1786485d9ac2c9a2d_1029x552.png) ***** 但是缓存空对象也会出现两个问题: 1. 空值做了缓存 ~~~ 意味着缓存层中存了更多的键,需要更多的内存空间来进行存储(如果是属于恶意攻击,消耗会更大),比较有效防止的方式是针对这类数据设置一个较短的过期时间,让他自动删除。 ~~~ 2. 缓存层与存储层的数据会出现一段时间的数据不一致,会对业务出现一定的影响 ~~~ 比如:此时我们对这个空数据设置了一个过期时间为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; } } ?> ``` ***** **2. 布隆过滤器** 在访问缓存层与存储层之前,将存在的key使用布隆过滤器提前保存起来,做一层拦截。 ![](https://img.kancloud.cn/8e/4d/8e4d1865f65327359daff0d08d882943_1072x552.png) ***** **什么是布隆过滤器?** 一种数据结构,是由一串很长的二进制向量组成,可以将其看成一个二进制数组。既然是二进制,那么里面存放的不是0,就是1,但是初始默认值都是0。 ![](https://img.kancloud.cn/f5/e4/f5e488e3c9cb301c93c5550b750af420_1010x328.png) ***** **布隆过滤器安装:** 1. 获取布隆过滤器安装包 ~~~ [root@localhost local]# git clone git://github.com/RedisLabsModules/rebloom ~~~ 2. 安装布隆过滤器 ~~~ [root@localhost local]# cd rebloom [root@localhost rebloom]# make ~~~ 安装完成之后会出现 redisbloom.so ![](https://img.kancloud.cn/1c/27/1c27e8bb99a56587bea8c6d6dacfb73e_1055x548.png) 3. 修改redis配置文件 ![](https://img.kancloud.cn/92/04/9204d52f180aa88a0cfc7f379c877d24_1024x694.png) 4. 完成之后重启redis即可。 ***** **关于布隆过滤器的命令:** ~~~ (1)BF.ADD bloom redis #新增数据 (2)BF.EXISTS bloom redis #判断数据 ~~~ ***** **解决缓存穿透问题:** ~~~php <?php public function get($id) { $key = "cache_".$id; $ret = Redis::get($key); if (empty($ret)) { $lua = <<<LUA_SCRIPT local ret = redis.call("BF.EXISTS",KEYS[1],ARGV[1]); return ret; LUA_SCRIPT; $eval = Redis::eval($lua,1,$key,$id); if ($eval) { $ret = DB::table("article")->where("id",$id)->first(); Redis::set($key,$ret); }else{ return null; } }else{ return $ret; } } ?> ~~~ 思路如下: ~~~ 当缓存层查询数据不存在时,进入布隆过滤器层,布隆过滤器会存储着mysql中已有的数据,通过判断布隆过滤器中是否存储我们需要查询的数据来确定数据是否在mysql服务器中,如果有则放行查询,没有就返回null ~~~ >[info] 缓存雪崩 由于缓存层承载着大量的请求,有效地保护了存储层,但是如果缓存层由于某些原因不能提供服务,于是所有的请求都会达到存储层,存储层的调用量会暴增,造成存储层也会级联宕机的情况。 ![](https://img.kancloud.cn/0f/cf/0fcf043ee686fd839db51e76313c67a8_1107x419.png) ***** **缓存穿透,击穿,雪崩对比:** 缓存雪崩:当**缓存服务器重启**或者**大量缓存集中在某一个时间段失效**,这样在失效的时候,也会给后端系统(比如DB)带来很大压力。 ***** 缓存击穿:**key对应的数据存在,但在redis中过期**,此时若有大量并发请求过来,这些请求发现缓存过期一般都会从后端DB加载数据并回设到缓存,这个时候大并发的请求可能会瞬间把后端DB压垮 ***** 缓存穿透: key对应的数据在**数据源并不存在**,每次针对此key的请求从缓存获取不到,请求都会到数据源,从而可能压垮数据源。比如用一个不存在的用户id获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库 >[info] 雪崩预防方案 预防和解决缓存雪崩问题,可以从以下三个方面进行着手。 1. **保证缓存层服务高可用性。** 和汽车一样都有多个轮胎一样,如果缓存层设计成高可用的, 即使个别节点,个别机器甚至是机房宕掉,依然可以提供服务,例如(Redis哨兵)Redis Sentinel和(Redis集群)Reis Cluster都实现了高可用。 2. **后端限流并降级。** 限流是只允许一定时间内多少用户来进行访问,溢出的我们就直接打回,避免全部负载全部到到mysql服务器,降级是牺牲一些业务,提升mysql服务器请求处理容量。 3. **过期时间错开。** 雪崩一般是指同一时刻出现大批量的数据过期导致请求全部打到mysql服务器,到 服务器宕机,那么我们可以通过限制缓存的过期时间来进行预防,不同的key会去设置不同的过期时 间。 ***** >[info] 缓存大总结 缓存分为: * 缓存击穿 * 缓存穿透 * 缓存雪崩 **共同点:** 全部在于请求全部打到mysql中,此时mysql的负载超出本身处理上限值。 导致服务宕机,产生连锁反应(其他应用也直接不再提供服务) ***** **产生的问题:** 击穿:热点数据(并不是很多)+ 过期 => 火的小说 => 负面新闻 雪崩:大批量的key失效,或者缓存服务器(重启)宕机 穿透:查询不存在的数据 => 爬虫,恶意攻击(模拟数据查询) ***** **解决方案:** 击穿:加锁(排他锁)、永不过期 穿透:缓存空对象、布隆过滤器(二进制 0 1,判断key是否在其中) 雪崩:集群高可用(缓存服务器宕机)、限流降级、过期时间错开