在 Spring Boot 项目中结合 Redis 解决缓存穿透问题(我这里面是通过Redis序列化的,键通过string,值是通过Jackson2JsonRedisSerializer来序列化的),可以通过以下方案实现:
1. 缓存穿透问题定义
- 现象:大量请求查询数据库中 不存在的数据(如无效的 ID),导致请求绕过缓存直接访问数据库,引发数据库压力。也就是说防止一些无效的信息来恶意的一直访问数据库来增加数据库的压力
- 原因:恶意攻击或无效请求导致缓存层无法拦截不存在的数据请求。
2. 解决方案及 Spring Boot 实现
方案 1:缓存空值(Null Object)
- 原理:将数据库中不存在的数据也缓存到 Redis,但设置较短的过期时间,避免频繁查询数据库。
- 实现步骤:
- 查询数据时优先查缓存。
- 若缓存未命中,则查数据库。
- 数据库不存在时,缓存空值。
Spring Boot 代码示例(使用Mybatis-plus):
@Service
public class ShopServiceImpl extends ServiceImpl<ShopMapper, Shop> implements IShopService {
@Autowired
private RedisTemplate<String,Object> redisTemplate;
// 查询商铺信息
@Override
public Result queryById(Long id) {
if (id == null){
return Result.fail("店铺id不能为空");
}
// 1.从redis缓存中查询店铺信息
String shopKey = CACHE_SHOP_KEY + id;
Shop shop = (Shop) redisTemplate.opsForValue().get(shopKey);
// 2.判断shop对象是否存在并且里面的属性值都不为空
if (shop != null && !EntityUtils.isAllFieldsEmpty(shop)){
// 3.如果存在,直接返回
return Result.ok(shop);
}
// 4.如果shop对象不存在或者是属性值全部是空,则从数据库中查询
Shop shop1 = this.getById(id);
// 5.判断是否可以查询的到shop1在数据库里面
if (shop1 == null){
// 6.防止缓存穿透,缓存空对象并设置短过期时间
redisTemplate.opsForValue().set(shopKey, new Shop(), 2, TimeUnit.MINUTES);
return Result.fail("店铺不存在");
}
// 7.写入缓存并设置随机过期时间,防止雪崩
redisTemplate.opsForValue().set(shopKey, shop1, 30, TimeUnit.MINUTES);
return Result.ok(shop1);
}
}
EntityUtils工具类用来判断实体类的属性值是否全部为空,是否为空的两个方法
package com.hmdp.utils;
import cn.hutool.core.util.ReflectUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import java.util.List;
import java.util.Arrays;
// 封装工具类 用于判断对象的属性值是否为空
public class EntityUtils {
// 判断对象所有属性值是否为空
public static boolean isAllFieldsEmpty(Object obj) {
if (obj == null) return true;
return Arrays.stream(ReflectUtil.getFields(obj.getClass())) // 修复点:使用 Arrays.stream()
.map(field -> ReflectUtil.getFieldValue(obj, field))
.allMatch(EntityUtils::isFieldValueEmpty);
}
// 判断对象单个属性值是否为空
private static boolean isFieldValueEmpty(Object value) {
if (value == null) return true;
if (value instanceof String && StrUtil.isBlank((String) value)) {
return true;
} else if (value instanceof List && ((List<?>) value).isEmpty()) {
return true;
} else if (value.getClass().isArray() && java.lang.reflect.Array.getLength(value) == 0) {
return true;
}
return !ObjectUtil.isNotNull(value);
}
}
说明:
容易犯错的地方是这个判断,在得到的shop实体类的时候不能确保里面的全部属性值都不为null,shop != null只能判断这个实体类是否存在,不能判断里面的属性值是否为null。所以需要编写一个工具类来判断这个属性问题
方案 2:布隆过滤器(Bloom Filter)
- 原理:通过布隆过滤器预先存储所有合法键,拦截非法请求(判断键不存在时直接返回)。
- 适用场景:数据范围固定且可预加载(如商品 ID 为自增数字)。
实现步骤:
- 初始化布隆过滤器:系统启动时加载所有合法键到布隆过滤器。
- 请求拦截:查询前先通过布隆过滤器校验键是否存在,不存在则直接拒绝。
Spring Boot 集成布隆过滤器
1. 方案选择
- Guava 布隆过滤器:适合单机场景,简单易用。
- RedisBloom 模块:适合分布式场景,数据共享。
这里以 Guava 为例(单机方案)。
2. 依赖引入
在 pom.xml
中添加 Guava 依赖:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
3. 初始化布隆过滤器
在应用启动时,加载所有合法数据标识(如商品 ID)到布隆过滤器。
@Component
public class BloomFilterInitializer {
private BloomFilter<Long> bloomFilter;
@Autowired
private ProductRepository productRepository;
@PostConstruct // 应用启动时初始化
public void init() {
// 1. 从数据库加载所有合法 ID 添加了一个数据库里面的ID式例
List<Long> productIds = productRepository.findAllProductIds();
// 2. 初始化布隆过滤器
// 参数:预期数据量 100000,误判率 0.1%
bloomFilter = BloomFilter.create(
Funnels.longFunnel(),
100000,
0.001
);
// 3. 将 ID 添加到布隆过滤器
productIds.forEach(bloomFilter::put);
}
public boolean mightContain(Long id) {
return bloomFilter.mightContain(id);
}
}
4. 业务层拦截非法请求
在查询数据前,先通过布隆过滤器校验请求的合法性。
@Service
public class ProductService {
@Autowired
private BloomFilterInitializer bloomFilter;
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public Product getProductById(Long id) {
// 1. 布隆过滤器校验
if (!bloomFilter.mightContain(id)) {
// 直接拦截非法请求
return null;
}
// 2. 查询缓存
String cacheKey = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(cacheKey);
if (product != null && !EntityUtils.isAllFieldsEmpty(product)) {
return product;
}
// 3. 缓存未命中,查询数据库
product = productRepository.findById(id).orElse(null);
// 4. 数据库不存在,缓存空值(短时间)
if (product == null) {
redisTemplate.opsForValue().set(cacheKey, "NULL", 5, TimeUnit.MINUTES);
return null;
}
// 5. 数据库存在,更新缓存
redisTemplate.opsForValue().set(cacheKey, product, 1, TimeUnit.HOURS);
return product;
}
}
关键优化点
1. 布隆过滤器参数选择
- 预期数据量(n):根据业务实际数据量预估,避免频繁扩容。
- 误判率(p):通常设置为
0.1%~1%
,内存占用公式:m = -n * ln(p) / (ln2)^2
。 - 哈希函数数量(k):最优值
k = (m/n) * ln2
。
2. 动态更新布隆过滤器
- 问题:新增数据时,布隆过滤器需同步更新。
- 解决方案:
- 方案 1:定期全量重建(适合数据变化少的场景)。
- 方案 2:监听数据库变更事件(如 Binlog),实时更新布隆过滤器。
3. 结合空值缓存
- 原因:布隆过滤器存在误判,需结合空值缓存拦截非法请求。
- 实现:当布隆过滤器判断可能存在但数据库查无数据时,缓存空值。
RedisBloom 分布式方案
若系统为分布式架构,可使用 Redis 的 RedisBloom 模块(需安装)。
1. RedisBloom 命令
# 添加元素
BF.ADD bloom_filter 12345
# 检查元素是否存在
BF.EXISTS bloom_filter 12345
2. Spring Boot 集成示例
@Service
public class RedisBloomService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
// 初始化布隆过滤器
public void initBloomFilter() {
// 创建布隆过滤器(容量 100000,误判率 0.1%)
redisTemplate.execute((RedisCallback<Object>) connection -> {
connection.execute("BF.RESERVE", "product_bloom", "0.001", "100000");
return null;
});
}
// 添加元素到布隆过滤器
public void addToBloomFilter(Long id) {
redisTemplate.execute((RedisCallback<Object>) connection -> {
connection.execute("BF.ADD", "product_bloom", id.toString().getBytes());
return null;
});
}
// 检查元素是否存在
public boolean existsInBloomFilter(Long id) {
return (Boolean) redisTemplate.execute((RedisCallback<Object>) connection -> {
return connection.execute("BF.EXISTS", "product_bloom", id.toString().getBytes());
});
}
}
方案对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
Guava 布隆过滤器 | 简单、内存高效 | 单机部署,重启后数据丢失 | 单机应用,数据量可控 |
RedisBloom | 分布式支持,数据持久化 | 需安装额外模块,略复杂 | 分布式系统,高可用需求 |
空值缓存 | 实现简单,无额外依赖 | 内存浪费(大量空值缓存) | 小规模数据,临时解决方案 |
方案 3:请求限流与熔断
- 原理:对高频无效请求进行限流,保护数据库。
- 实现工具:使用 Resilience4j 或 Sentinel 实现限流熔断。
Resilience4j 限流示例:
@Configuration
public class RateLimitConfig {
@Bean
public RateLimiterRegistry rateLimiterRegistry() {
return RateLimiterRegistry.of(
RateLimiterConfig.custom()
.limitForPeriod(100) // 每秒钟允许 100 次请求
.limitRefreshPeriod(Duration.ofSeconds(1))
.timeoutDuration(Duration.ofMillis(500))
.build()
);
}
}
@Service
public class ProductService {
@Autowired
private RateLimiterRegistry rateLimiterRegistry;
public Product getProductById(Long id) {
RateLimiter rateLimiter = rateLimiterRegistry.rateLimiter("productQuery");
if (rateLimiter.acquirePermission()) {
// 正常处理逻辑
} else {
throw new RuntimeException("请求过于频繁");
}
}
}
3. 组合策略(推荐)
- 步骤 1:使用 布隆过滤器 拦截明显无效的请求。
- 步骤 2:对可能误判的请求,结合 空值缓存 减少数据库压力。
- 步骤 3:对高频请求进行 限流熔断,双重保护数据库。
4. 其他注意事项
- 缓存雪崩预防:为缓存设置随机过期时间,避免同一时间大量缓存失效。
- 热点数据预加载:针对高频访问数据,提前加载到缓存。
- 监控与告警:监控缓存命中率和数据库 QPS,及时发现异常。
通过上述方案,可以在 Spring Boot 项目中有效解决缓存穿透问题,保障系统的高可用性和性能。