缓存穿透
缓存穿透是用户请求不存在的数据,导致数据库压力增大的行为。
当用户发送请求时,由于请求的数据不存在于缓存,也不存在于数据库,但是这个请求还是会访问到数据库。如果短时间内大量这种请求发送过来,会导致数据库宕机。
解决方案
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) { String key = "User::" + id; String userJsonStr = (String) redisTemplate.opsForValue().get(key); log.info("查询缓存:{}",userJsonStr); if (StrUtil.isNotBlank(userJsonStr)) { if ("{}".equals(userJsonStr)) { return null; } 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]()
1 2 3 4 5
| <dependency> <groupId>org.redisson</groupId> <artifactId>redisson</artifactId> <version>3.27.2</version> </dependency>
|
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 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; 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)。
实现复杂度: 需要预热过滤器,且要维护逻辑以保持过滤器与数据库的同步。