1、缓存穿透的概念
缓存穿透是指查询一个根本不存在的数据,缓存层和存储层都不会命中,但是出于容错的考虑,从存储层查不到数据则不写入缓存层,整个过程分为如下 3 步:
- 缓存层不命中
- 存储层不命中,所以不将空结果写回缓存
- 返回空结果
缓存穿透将导致不存在的数据每次请求都要到存储层去查询,失去了缓存保护后端存储的意义。
缓存穿透问题可能会使后端存储负载加大,很多情况下后端存储不具备高并发性,可能造成后端存储宕掉。
如果在生产环境中发现大量存储层空命中,可能就是出现了缓存穿透问题。
2、造成缓存穿透的原因
- 业务自身代码或者数据出现问题,一直在访问各种不存在的数据;
- 恶意攻击、爬虫等造成大量空命中
3、解决方案一:缓存空对象
存储层不命中后,仍然将空对象保留到缓存层中(key=请求的key, value=nil),之后再访问这个数据将会从缓存中获取,这样就保护了后端数据源。
此方案可能存在的问题:
空值做了缓存,意味着缓存层中存了更多的元素,需要更多的内存空间。 而且一旦发生Redis数据淘汰,存储的空值可能会反过来挤压正常缓存的生存空间。 缓解的方法是可以针对这类数据设置一个较短的过期时间。
如果是恶意攻击的话,假设对方不停查询id=-1,-2,-3……的数据(id++),缓存空值实际上无效
3-1、缓存空对象方案的改进
为了提升安全性,更进一步的做法,是将空对象放在专门的存储空间中,不与正常值共用空间,也就是建立空值专用的Redis实例。
否则如果持续遭受攻击,Redis空间被塞满,并且数据淘汰策略被设定了LRU的话,系统会优先删除正常值,直至Redis被空对象塞满。
这种做法提升了安全性,但是也提升了系统复杂度,需要根据实际情况酌情采用。还要注意key对应的正常对象创建以后,要使用消息通知等方式删除空对象确保缓存正常工作。
4、解决方案二:布隆过滤器
在访问缓存层和存储层之前,将存在的 key 用布隆过滤器提前保存起来,做第一层拦截(预处理)。
先说一下处理流程,后面简明地说明什么是布隆过滤器(Bloom Filter)。
① 事先将存储层中存在的数据的key存储在布隆过滤器中(一般offline处理)
②③ 客户端访问数据时,先在布隆过滤器做预处理,一旦判断数据不存在的话则直接返回不存在的提示
④ 请求通过了布隆过滤器的筛选后,到达缓存层,如果有缓存,则向客户端返回结果
⑤⑥⑦ 缓存层中不存在请求的数据,则访问存储层,将数据写入缓存层,并向客户端返回结果
4-1、布隆过滤器的原理
这一解决方案的关键在于布隆过滤器。
布隆过滤器是一个很长的二进制向量和一系列随机映射函数,用于检索一个元素是否在一个集合中。
布隆过滤器的原理是,当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。检索时,我们只要看看这些点是不是都是1就知道集合中有没有它了:如果这些点有任何一个0,则被检元素一定不在;如果都是1,则被检元素很可能在。
下面用一个简化的例子来说明存储和判断过程,例子所使用的所谓散列函数就是直接转换成二进制。
假设有三条数据需要存储:
{
"dates": [
{
"key": 1,
"value": "Google"
},
{
"key": 2,
"value": "Bing"
},
{
"key": 23,
"value": "Yahoo"
}
]
}
Bloom Filter 初始化 (1 byte / 8 bits)
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
---|
存储第一个key,1 = 00000001
0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 |
---|
存储第二个key,2 = 00000010
0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
---|
存储第三个key,23 = 00010110。此时倒数第二位已经是1了,但是无所谓,直接覆写即可
0 | 0 | 0 | 1 | 0 | 1 | 1 | 1 |
---|
Bloom Filter 构建完毕,结果为 00010111。
如果请求数据的key为55的话,55 = 00110111
请求数据的key,第三位是1,但是Bloom Filter里面第三位是0,那么这个key是一定不存在的,直接拦截,不向Redis发起请求。
如果请求数据的key为3的话,3 = 00000011
请求数据的key里面是1的位,在Bloom Filter里面都是1,认为这个key有可能存在,将请求交给Redis处理。这就是一次误识别。
4-2、布隆过滤器的优缺点
布隆过滤器的优点是空间效率和查询时间远超一般算法,存储空间和插入/查询时间都是常数 O(n)。
而且它使用的散列函数相互之间没有关系,可以并行实现。
缺点是有一定的误识别率,并且删除困难。
误识别的情况上面已经举过例子了,随着存入的元素数量增加,误算率会随之增加。
至于删除困难这件事情,每一个“1”都有可能是多个元素共用的,不能轻易删除;而且也不能100%保证你要删除的元素现在就在布隆过滤器里面存在。
4-3、缓存中使用布隆过滤器的应用场景
在缓存层之前追加布隆过滤器这一方案,有一个很大的问题,就是时效性,通常布隆过滤器的内容是提前计算好的——比如每小时更新。那么最新入库的数据在过滤器中不存在,就会造成访问不到数据的情况。
因此这种方法适用于数据命中不高,数据相对固定、实时性低(通常是数据量很大)的场景。
代码维护较为复杂,但是缓存空间占用少。
在实际应用中,可以利用 Redis 的 Bitmaps 这种数据类型实现布隆过滤器。