网站Logo Ilren 小记

Spring Boot 缓存穿透解决方案

jack
3
2023-12-25

一、什么是缓存穿透?

缓存穿透是指:客户端请求的数据在缓存中查不到,数据库中也没有,导致每次请求都打到数据库

场景示例:

  • 用户请求一个 id=-1 的数据。

  • 缓存中查不到,去数据库查,也没有该数据。

  • 每次请求都绕过缓存打到数据库。

  • 大量这类请求形成“洪水攻击”,压垮数据库。

二、缓存穿透的常见原因

场景

描述

无效参数

请求参数非法,如负数、空串、特殊字符等

数据本身不存在

请求的是逻辑已删除或未创建的数据

恶意攻击

攻击者发起大量不存在 key 的请求

缓存未命中

未设置缓存或缓存过期,导致频繁访问数据库

三、Spring Boot 缓存穿透的解决方案

✅ 方案一:缓存空对象

当数据库查询结果为空时,也将结果缓存起来,设置短期过期时间。

@Cacheable(value = "userCache", key = "#userId", unless = "#result == null")
public User getUserById(Long userId) {
    return userRepository.findById(userId).orElse(null);
}

但默认情况下,Spring Cache 不缓存 null,需要额外配置或处理。

改进:手动缓存空值

public User getUserById(Long userId) {
    String cacheKey = "user:" + userId;
    User user = redisTemplate.opsForValue().get(cacheKey);

    if (user == null) {
        user = userRepository.findById(userId).orElse(null);
        redisTemplate.opsForValue().set(cacheKey, user == null ? new NullUser() : user, Duration.ofMinutes(5));
    }

    return user instanceof NullUser ? null : user;
}

建议使用对象标记类(如 NullUser)防止序列化异常。

✅ 方案二:参数校验拦截非法请求

在接口层添加校验逻辑,拦截明显非法的请求,避免无意义查询。

@GetMapping("/user/{id}")
public ResponseEntity<?> getUser(@PathVariable Long id) {
    if (id <= 0) {
        return ResponseEntity.badRequest().body("Invalid ID");
    }
    return ResponseEntity.ok(userService.getUserById(id));
}

✅ 方案三:布隆过滤器预判数据存在性

利用布隆过滤器(Bloom Filter)提前判断 key 是否可能存在,若确定不存在则不查询数据库。

// 初始化布隆过滤器
BloomFilter<Long> bloomFilter = BloomFilter.create(Funnels.longFunnel(), 1000000);

// 查询时判断
if (!bloomFilter.mightContain(userId)) {
    return null; // 直接拒绝
}

推荐使用 Google Guava 或 Redis Bloom 插件(如 RedisBloom 模块)。

✅ 方案四:异步预热+定时刷新缓存

对于常用数据,可以在系统启动或定时任务中进行缓存预热,降低首次请求开销。

@Scheduled(fixedRate = 10 * 60 * 1000)
public void refreshHotUsers() {
    List<Long> hotUserIds = getHotUserIds();
    for (Long id : hotUserIds) {
        User user = userRepository.findById(id).orElse(null);
        redisTemplate.opsForValue().set("user:" + id, user, Duration.ofHours(1));
    }
}

✅ 方案五:后端限流/防刷

使用请求限流、防爬虫组件(如 Sentinel、Bucket4j)拦截恶意高频请求。

四、组合策略推荐(实战)

建议采用“三位一体防御”思路:

  1. 参数校验:前置拦截无效请求。

  2. 空值缓存:缓解正常请求导致的穿透。

  3. 布隆过滤器:过滤明显不存在的 key。

五、Spring Boot 中集成 Redis 缓存空值示例

@Service
public class ProductService {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Autowired
    private ProductRepository productRepository;

    public Product getById(Long id) {
        String key = "product:" + id;

        Object cached = redisTemplate.opsForValue().get(key);
        if (cached != null) {
            return cached instanceof NullProduct ? null : (Product) cached;
        }

        Product product = productRepository.findById(id).orElse(null);
        if (product == null) {
            redisTemplate.opsForValue().set(key, new NullProduct(), 5, TimeUnit.MINUTES);
            return null;
        }

        redisTemplate.opsForValue().set(key, product, 30, TimeUnit.MINUTES);
        return product;
    }

    static class NullProduct implements Serializable {}
}

六、总结

方案

适用场景

优点

缺点

缓存空值

查询不到真实数据时

简单有效

占用缓存空间,需控制 TTL

参数校验

拦截非法请求

实现简单

依赖接口设计

布隆过滤器

高并发、海量 ID 场景

精度高,空间效率好

有一定误判概率

异步预热

热数据预测

减少首次延迟

不适合全量数据

后端限流/防刷

防攻击防爆库

有效保护数据库

引入新组件,需配置

动物装饰