1. 简介
1.1 概念
缓存穿透是指查询一个不存在的数据,缓存层与存储层都不会命中,通常出于容错的考虑,如果从存储层查不到数据则不写入缓存层。大致可分为以下3步:
- 缓存层不命中
- 存储层不命中,不将空结果写回缓存
- 返回空结果
描述:缓存穿透将导致不存在的数据每次请求都需要到存储层去查询,失去了缓存保护后端存储的意义。缓存穿透问题可能会使后端存储负载加大,由于很多存储不具备高并发性,甚至可能造成后端存储宕机,通常可以在程序中分别统计总调用数,缓存层命中数,存储层命中数,如果发现大量存储层命中为空,可能就是出现了缓存穿透问题。
1.2 原因
- 自身的业务代码或者数据出现问题
- 一些恶意攻击,爬虫等造成大量空命中
1.3 解决方案
-
缓存空对象:在缓存没有命中的情况下,将空对象作为数据保留到缓存中,之后再来访问这个数据将会从缓存中进行获取,这样就保护的后端数据源,不会因为缓存穿透,导致大量请求到存储层,发生服务器宕机,或者拖累其他应用。但缓存空值下面几个问题任然需要考虑的:
①、空值做了缓存意味着缓存层中存了更多的键,需要更多的内存空间来进行存储(如果是属于恶意攻击,消耗会更大),比较有效防止的方式是针对这类数据设置一个较短的过期时间,让他自动删除。
②、缓存层与存储层的数据会出现一段时间的数据不一致,会对业务出现一定的影响。假如我们对这个空数据设置了过期时间为5分钟,但是这个时候存储层添加了这条数据,那这段时间就会出现缓存层与存储层数据不一致的情况,针对这种情况我们可以使用消息系统或者其他方式消除缓存层存储的空对象。 -
布隆过滤器:在缓存层与存储层之间,将存在的key使用布隆过滤器保存起来,做一层拦截
1.4 布隆过滤器
1.4.1 布隆过滤器介绍
-
概念:一种数据结构,由一串很长的二进制向量组成,可以将其看成一个二进制数组,里面存放的不是0,就是1,但是初始默认值都是0。
-
布隆过滤器新增数据时,我们通过多个hash函数,算出一个值,然后将这个值所在的方格置为1,如下图所示,hash1(mossil)=3,那么在第4个格子将0变为1(数组是从0开始计数的),hash2(mossil)=6,那么将第7个格子置位1,依次类推。
-
如何判断数据是否存在于这个布隆过滤器中呢?
-
只需要将这个新的数据通过上面自定义的几个哈希函数,分别算出各个值,然后看其对应的地方是否都是1,如果存在一个不是1的情况,那么我们可以说,该新数据一定不存在于这个布隆过滤器中。
-
反而言之,如果通过哈希函数算出来的值,对应的地方都是1,但是我们并不能肯定这个数据一定存在于这个布隆过滤器中,因为存在误差问题,多个不同的数据通过hash函数算出来的结果是会有重复(hash碰撞)。
-
结论:布隆过滤器可以判断某个数据一定不存在,但是无法判断一定存在。
1.4.2 布隆过滤器优缺点
- 优点:二进制组成的数组,占用内存极少,并且插入和查询速度都足够快。
- 缺点:随着数据的增加,误判率会增加;还有无法判断数据一定存在;另外还有一个重要缺点,无法删除数据。
1.4.3 布隆过滤器实践
- 安装
git clone git://github.com/RedisLabsModules/rebloom
cd rebloom
make
安装完成后会生成redisbloom.so,将其路径配置到redis.conf配置文件即可,完成后需重启redis,启动时需要指定配置文件,否则配置可能不会生效,如:./src/redis-server redis.conf。
- 相关命令
BF.ADD bloom redis #新增数据
BF.EXISTS bloom redis #判断数据
- 实际应用( 解决缓存穿透),解决思路:当缓存层查询数据不存在时,进入布隆过滤器层,布隆过滤器会存储着mysql中已有的数据,通过判断布隆过滤器中是否存储我们需要查询的数据来确定数据是否在mysql服务器中,如果有则放行查询,没有就返回null,核心代码如下:
public function get($id)
{
$key = "cache_".$id;
$res = Redis::get($key);
if (empty($res)) {
$lua = <<<LUA_SCRIPT
local ret = redis.call("BF.EXISTS",KEYS[1],ARGV[1]);
return ret;
LUA_SCRIPT;
$eval = Redis::eval($lua,1,$key,$id);
// 布隆过滤器中存在,去存储层去,不存在返回null
if ($eval) {
$result = DB::table("article")->where("id",$id)->first();
Redis::set($key,$result);
} else {
return null;
}
} else {
return $ret;
}
}