缓存穿透
查询不存在的数据,因为 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:缓存空对象(最常用)
查询数据库发现 没有数据
不要直接返回。
而是:
if (user == null) {
redisTemplate.opsForValue().set(key, "NULL", 5 TimeUnit.MINUTES);
}将 NULL 缓存到 Redis 中。
流程:
第一次
Redis 没有
↓
数据库没有
↓
Redis存NULL第二次
Redis有 NULL
↓
直接返回数据库不会被再次访问。
注意:一般我们设置空对象的过期时间要短一些。
代码示例
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左右
节省了几十倍内存。
所以一些大型项目都会使用。