一、什么是缓存穿透?
缓存穿透是指:客户端请求的数据在缓存中查不到,数据库中也没有,导致每次请求都打到数据库。
场景示例:
用户请求一个
id=-1
的数据。缓存中查不到,去数据库查,也没有该数据。
每次请求都绕过缓存打到数据库。
大量这类请求形成“洪水攻击”,压垮数据库。
二、缓存穿透的常见原因
三、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)拦截恶意高频请求。
四、组合策略推荐(实战)
建议采用“三位一体防御”思路:
参数校验:前置拦截无效请求。
空值缓存:缓解正常请求导致的穿透。
布隆过滤器:过滤明显不存在的 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 {}
}