在高并发场景下,缓存是提升系统性能的关键技术之一。但当恶意用户或爬虫访问大量不存在的数据时,会导致所有请求直接打到数据库,缓存无法生效,进而导致数据库压力陡增,这种现象被称为缓存穿透。
本文将介绍 布隆过滤器(Bloom Filter)+缓存空值 如何有效防止缓存穿透,并基于 Spring Boot + MyBatis-Plus + Redis 进行代码实现。
1. 什么是缓存穿透?
缓存穿透 是指:
- 客户端请求的数据在缓存和数据库中都不存在。
- 由于缓存没有这个数据,所有请求都会直接打到数据库。
- 如果请求量大,会严重影响数据库性能,甚至导致宕机。
示例:
假设我们的数据库里只有 ID=1,2,3 的店铺信息,但某些用户频繁请求 不合理ID,如ID=-1:
客户端请求 ID=-1 --> 缓存无数据 --> 数据库也无数据 --> 请求直接打数据库
这些请求永远不会被缓存命中,导致 数据库负载过高。
布隆过滤器的特点:
操作 | 说明 |
---|---|
可能存在 | 数据可能存在,但不能 100% 确定 |
一定不存在 | 该数据绝对不存在 |
布隆过滤器的核心思想是存储的是元素的哈希值,可以快速判断一个元素是否可能存在,这样可以有效拦截非法请求,减少对数据库的直接访问。
3. 在 Spring Boot 项目中实现布隆过滤器
3.1 引入 Guava 依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version> <!-- 最新版本 -->
</dependency>
3.2 初始化布隆过滤器
在应用启动时,从数据库加载所有合法的店铺 ID,并存入布隆过滤器
package com.hmdp.Interceptor;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.hmdp.entity.Shop;
import com.hmdp.mapper.ShopMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
import java.util.List;
import java.util.stream.Collectors;
@Component
@Slf4j
public class BloomFilterInitializer {
@Autowired
private ShopMapper shopMapper;
private static BloomFilter<Long> bloomFilter;
@PostConstruct
public void initBloomFilter() {
// 1. 从数据库加载所有存在的店铺ID
List<Long> ids = shopMapper.selectObjs(
Wrappers.<Shop>query().select("id")
).stream()
.map(obj -> ((Number) obj).longValue()) // 避免 BigInteger 转换错误
.collect(Collectors.toList());
// 2. 初始化布隆过滤器(预期元素 100 万,误判率 1%)
bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
1_000_000L,
0.01
);
// 3. 将数据库中的店铺 ID 加入布隆过滤器
ids.forEach(bloomFilter::put);
log.info("布隆过滤器初始化完成,共存入 {} 个店铺 ID", ids.size());
}
public static BloomFilter<Long> getBloomFilter() {
return bloomFilter;
}
}
📌 代码解析
selectObjs(Wrappers.<Shop>query().select("id"))
:从数据库查询所有店铺 ID。BloomFilter.create(Funnels.longFunnel(), 1_000_000L, 0.01)
:- 预计存储 100 万 个 ID。
- 误判率 1%,即1% 的可能性会误判为“可能存在”。
3.3 在查询逻辑中使用布隆过滤器
在 ShopServiceImpl
中修改 queryById(Long id)
方法,先通过布隆过滤器判断 ID 是否可能存在:
@Override
public Result queryById(Long id) {
// 0. 使用布隆过滤器拦截不存在的 ID
if (!BloomFilterInitializer.getBloomFilter().mightContain(id)) {
log.debug("店铺不存在,id={}", id);
return Result.fail("店铺不存在!");
}
String key = RedisConstants.CACHE_SHOP_KEY + id;
// 1. 先查询 Redis 缓存
String shopJson = stringRedisTemplate.opsForValue().get(key);
// 2. 缓存命中,直接返回
if (StrUtil.isNotBlank(shopJson)) {
return Result.ok(JSONUtil.toBean(shopJson, Shop.class));
}
// 3. Redis 已缓存空值(防止缓存穿透)
if (shopJson != null) {
return Result.fail("店铺不存在");
}
// 4. 查询数据库
Shop shop = getById(id);
if (shop == null) {
// 缓存空值(短时间缓存,防止缓存穿透)
stringRedisTemplate.opsForValue().set(key, "", RedisConstants.CACHE_NULL_TTL, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
// 5. 写入 Redis
stringRedisTemplate.opsForValue().set(
key, JSONUtil.toJsonStr(shop), RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES
);
return Result.ok(shop);
}
4. 方案对比
方案 | 缓存穿透风险 | 适用场景 | 额外开销 |
---|---|---|---|
不做处理 | 高 | 低并发、小型系统 | 无 |
缓存空值 | 低 | ID 范围有限的场景 | 占用 Redis 内存 |
布隆过滤器 | 极低 | 高并发、大数据量 | 误判 1%,但影响较小 |