redis:缓存穿透

1.什么是缓存穿透

缓存穿透 :**缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。**可能是恶意用户伪造不存在的id发起请求,会导致缓存和数据库都无法查到,导致所有请求打到数据库上,数据库可能会因为扛不住压力而挂掉。

2.解决方案

常见的解决方案有两种:

  • 缓存空对象

    • 优点:实现简单,维护方便
    • 缺点:
      • 额外的内存消耗
      • 可能造成短期的不一致
  • 布隆过滤

    • 优点:内存占用较少,没有多余key
    • 缺点:
      • 实现复杂
      • 存在误判可能

    1653326156516

2.1缓存空对象

**缓存空对象思路:**当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库,简单的解决方案就是哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据就不会进入到缓存了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
/**
* 解决缓存穿透
* @param keyPrefix redis中key前缀
* @param id 参数id
* @param type 返回值类型
* @param dbFallBack 要执行的数据库操作
* @param time 时间
* @param unit 时间单位
* @return
* @param <R> 返回值泛型
* @param <ID> 参数泛型
*/
public <R,ID> R queryWithPassThrough(String keyPrefix, ID id, Class<R> type, Function<ID,R> dbFallBack,Long time,TimeUnit unit) {
//1.从redis中查询
String key = keyPrefix+ id;
String json = stringRedisTemplate.opsForValue().get(key);
//2.存在 直接返回
if (StrUtil.isNotBlank(json)){
return JSONUtil.toBean(json, type);
}
if(json!=null){
//是空值(空字符串) ""
return null;
}
//3.不存在 从数据库中查询
R r = dbFallBack.apply(id);
//4.数据库中不存在 返回错误信息
if(r==null){
//将空值也写回redis 预防缓存穿透
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return null;
}
//5.数据中存在 返回信息 并写回redis 设置过期时间为30分钟
this.set(key,r, time, unit);
return r;
}

2.2 布隆过滤器

布隆过滤器底层是使用bit数组存储数据,该数组中的元素的默认值是0。而我们需要使用布隆过滤来解决缓存穿透问题,就需要将数据库中的数据缓存到布隆过滤器中(往往缓存key值)。在布隆过滤器第一次初始化的时候,会把数据库中所有的key(主键),经过一系列的hash算法(比如:三次hash算法)计算,每个key都会计算出多个位置,然后把这些位置上的元素值都设置为1.

img

之后,有用户key请求过来的时候,再用相同的hash算法计算位置。

  • 如果多个位置中的元素值都是1,则说明该key在数据库中已存在。这时允许继续往后面操作。
  • 如果有1个以上的位置上的元素值是0,则说明该key在数据库中不存在。这时可以拒绝该请求,而直接返回。

假设布隆过滤器判断这个数据不存在,则直接返回

这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突,因为存在hash冲突,所以说不同的key可能会计算出相同的位置。

img

**布隆过滤器的特点:判断不存在,则该数据一定不存在,如果存在,则该数据可能存在;**如果想减少hash冲突,可以适当的增加hash函数。比如三次hash增加到五次hash。

另外使用布隆过滤器还需要考虑到当数据库中数据发生更新的时候,就会导致数据库中的数据和布隆过滤器中的数据不一致问题,这时候就需要进行数据同步,如果数据同步失败了(比如网络原因),可能就会发生数据库中存在的数据用户查找不到的问题!

布隆过滤器实现起来比较困难,实际应用中可以使用现成的布隆过滤器实现:

比如:Google 开发著名的 Guava 库就提供了布隆过滤器(Bloom Filter)的实现。在基于 Maven 的 Java 项目中要使用 Guava 提供的布隆过滤器,只需要引入以下坐标:

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.0-jre</version>
</dependency>

在导入 Guava 库后,我们可以通过 BloomFilter.create 方法来创建一个布隆过滤器