Skip to content

缓存穿透

查询不存在的数据,因为 Redis 中没有,数据库中也没有,导致每次请求都会访问数据库,大量请求可能压垮数据库。

什么是缓存穿透(Cache Penetration)

正常的流程是:

用户请求

Redis
    ↓(没有)
数据库

写入Redis

返回

在第二次查询时

Redis 命中

直接返回

问题:当查询的数据在 数据库 中也不存在时

以查询用户 user:-1 为例

数据库中没有这个数据,因此第一次查询时:

Redis 没有

数据库没有

返回 null

第二次:

Redis 没有

数据库没有

返回 null

第三次:

Redis 没有

数据库没有

因为数据库中没有,所以每次都返回 null,导致无法缓存,请求每次都直接打到数据库上。

如果有人恶意攻击

假设:

user:-1
user:-2
user:-3
user:-4

每秒发起 10000 次请求,那么这 10000 次请求全部进入数据库。

Redis 完全失去作用。

这就是

缓存穿透

即:

请求穿过 Redis
直接打到数据库

解决方案

方案1:缓存空对象(最常用)

查询数据库发现 没有数据

不要直接返回。

而是:

java
if (user == null) {
    redisTemplate.opsForValue().set(key, "NULL", 5 TimeUnit.MINUTES);
}

NULL 缓存到 Redis 中。

流程:

第一次

Redis 没有

数据库没有

Redis存NULL

第二次

Redis有 NULL

直接返回

数据库不会被再次访问。

注意:一般我们设置空对象的过期时间要短一些。

代码示例

java
public User getUser(Long id) {
    String key = "user:" + id;

    Object obj = redisTemplate.opsForValue().get(key);

    if (obj != null) {
        if ("NULL".equals(obj)) {
            return null;
        }

        return (User)obj;
    }

    User user = userMapper.selectById(id);

    if (user == null) {
        redisTemplate.opsForvalue().set(key, "NULL", 5, TimeUnit.MINUTES);

        return null;
    }

    redisTemplate.opsForValue().set(key, user, 30, TimeUnit.MINUTES);

    return user;
}

方案2:布隆过滤器

大型系统:

  • 淘宝
  • 京东
  • 美团

可能会使用:

Bloom Filter(布隆过滤器)

流程:

请求

布隆过滤器

不存在

直接返回

数据库根本不会访问。

这个实现比较复杂,还是回到最初的问题,大量访问数据库中不存在的用户时,方案1:缓存空对象 虽然能解决但是还带来了一些问题。

如果时被攻击,大量的不存在的用户打到服务器上,Redis会缓存大量的空对象。

user:-1 = NULL
user:-2 = NULL
user:-9999999 = NULL

大量无意义的 Key 非常浪费内存空间。

布隆过滤器的思想

假设有数据库的用户ID

1
2
3
5
8
10

先把这些ID放到一个特殊的结构。

以后请求: 1000000

先问过布隆过滤器,这个ID可能存在吗。

如果答案:一定不存在

直接返回,根本不访问 Redis,也不会访问数据库。

流程:

请求

Bloom Filter

不存在

直接返回

布隆过滤器的原理

核心

位数组(BitMap)
+
多个哈希函数

假设ID最大长度: 16位

初始位数组:0000000000000000

加入:1

经过三个哈希函数:

hash1(1) = 2
hash2(1) = 5
hash3(1) = 8

这三个哈希的出来的树要在位数组上标记出来(也就是第2位、第5位、第8位)

位数组:
0 0 1 0 0 1 0 0 1 0 0 0 0 0 0 0
    |     |     |
0 1 2 3 4 5 6 7 8

加入:5

同样按照上述流程计算三个哈希值然后假如位数组,得到

0100010010100000

注意位数组只有一个,一直依赖都只操作这一个位数组。

查询

查询:100

通过哈希:

hash1(100) = 2
hash2(100) = 9
hash3(100) = 15

如果位数组 2、9、15 中 有一个是0,那么 100一定不存在

如果 2、9、15 都是1,那么 100有可能存在(有可能,不是一定存在)

布隆过滤器的特点

一旦说不存在 100% 不存在

一旦说存在,可能 存在

可能存在误判,这叫 误报(False Positive)

一次误判,最多查一次数据库,代价很小,因此允许误判。

内存优势

假设:1亿用户ID

HashSet:几个GB

布隆过滤器:100MB左右

节省了几十倍内存。

所以一些大型项目都会使用。