缓存空值和布隆过滤器解决缓存穿透
SryYa Three

缓存穿透

缓存穿透是用户请求不存在的数据,导致数据库压力增大的行为。
当用户发送请求时,由于请求的数据不存在于缓存,也不存在于数据库,但是这个请求还是会访问到数据库。如果短时间内大量这种请求发送过来,会导致数据库宕机。

解决方案

1.缓存空值(缓存null)

image

在建立缓存环节,如果数据库不存在请求的数据,便将空值(null)返回,在缓存中建立一个为null的缓存。后续请求过来时便能从缓存中获取数据,避免了数据库的压力。

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
public User selectUser(Integer id) {  
//生成缓存key
String key = "User::" + id;
//获取对象Json字符串
String userJsonStr = (String) redisTemplate.opsForValue().get(key);
log.info("查询缓存:{}",userJsonStr);
//如果缓存中存在值,则从缓存返回
if (StrUtil.isNotBlank(userJsonStr)) {
if ("{}".equals(userJsonStr)) {
return null;
}
//转换成Json对象并返回
return JSONUtil.toBean(userJsonStr, User.class);
}
//缓存中不存在
//查询数据库
User user = userMapper.selectUser(id);
log.info("查询数据库:{}",user);
if (user == null) {
//设置空缓存,注意设置短过期时间
redisTemplate.opsForValue().set(key,"{}",2,TimeUnit.MINUTES);
return null;
}
//设置正常缓存
redisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(user));
return user;
}

2.布隆过滤器

image

  • 引入redisson
1
2
3
4
5
<dependency>  
<groupId>org.redisson</groupId>
<artifactId>redisson</artifactId>
<version>3.27.2</version>
</dependency>
  • 配置redissonClient
1
2
3
4
5
6
7
8
@Bean  
public RedissonClient redissonClient() {
Config config = new Config();
config.useSingleServer()
.setAddress("redis://127.0.0.1:6379")
.setDatabase(0);
return Redisson.create(config);
}
  • 配置布隆过滤器
1
2
3
4
5
6
7
@Bean  
public RBloomFilter<Integer> userIdBloomFilter(RedissonClient redissonClient) {
log.info("创建布隆过滤器");
RBloomFilter<Integer> bloomFilter = redissonClient.getBloomFilter("bf:user:id");//设置布隆过滤器名称
bloomFilter.tryInit(1_000_000, 0.01);//设置存储大小,误判率
return bloomFilter;
}
  • 使用布隆过滤器
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
//利用PostConstruct注解初始化布隆过滤器
@PostConstruct
public void initBloomFilter() {
List<Integer> ids = userMapper.selectAllIds();
log.info("布隆过滤器将要加入的id:{}", ids);
ids.forEach(bloomFilter::add);
}

public User selectUser(Integer id) {
String key = "User::" + id;
//布隆过滤器拦截非法 ID
if (!bloomFilter.contains(id)) {
log.info("布隆过滤器拦截非法id: {}", id);
return null;
}
//先查缓存
String userJson = ((String) redisTemplate.opsForValue().get(key));
if (userJson != null) {
if ("{}".equals(userJson)) {
return null;
}
User user = JSONUtil.toBean(userJson, User.class);
log.info("缓存查询: {}", user);
return user;
}
//查数据库
User user = userMapper.selectUser(id);
log.info("数据库查询: {}", user);
//正常数据缓存
redisTemplate.opsForValue().set(key, JSONUtil.toJsonStr(user), 30, TimeUnit.MINUTES);
return user;
}

优缺点分析

缓存空值

优点

  • 简单直接: 实现成本极低,不需要额外的组件或复杂的算法。

  • 数据一致性好: 如果数据库后来新增了这条数据,只需在写入时更新缓存,或者等短期的 TTL(过期时间)失效即可。

  • 支持精确匹配: 能够完全消除对同一个不存在 Key 的重复攻击。

缺点

  • 空间浪费: 如果攻击者随机生成大量不存在的 Key,缓存层会充斥着大量的空对象,挤占有效数据的内存空间。
  • 延迟性: 虽然设置了 TTL,但在数据从无到有的间隙,用户仍可能读到旧的空结果。

布隆过滤器

优点

  • 极高的空间效率: 无需存储完整的 Key,只需占用极小的位空间即可处理海量数据。

  • 性能极佳: 查询速度极快,时间复杂度为 $O(k)$,$k$ 为哈希函数个数,与数据量大小无关。

  • 防范大规模散列攻击: 面对海量随机 Key 攻击时,比缓存空值更省内存。

缺点

  • 存在误判率(False Positives): 布隆过滤器说“不存在”一定不存在,但说“存在”时,有极小概率其实不存在(哈希碰撞)。这意味着仍有少量请求会穿透到数据库。

  • 删除困难: 标准的布隆过滤器不支持删除操作。一旦数据库删除了数据,过滤器里的标记无法轻易撤销(除非使用变体 Counting
    Bloom Filter)。

  • 实现复杂度: 需要预热过滤器,且要维护逻辑以保持过滤器与数据库的同步。

由 Hexo 驱动 & 主题 Keep
总字数 28.1k 访客数 访问量