Spring Boot 中如何解决 Redis 的缓存穿透、缓存击穿、缓存雪崩?
缓存是提升系统性能的重要手段,但在高并发场景下,可能会遇到缓存穿透、缓存击穿和缓存雪崩等问题。本文将介绍这三类问题的定义、常见场景及解决方法。
缓存穿透
什么是缓存穿透?
缓存穿透指缓存系统无法命中某些查询的数据,导致每次查询都直接访问数据库,从而造成数据库压力增大。
常见场景包括:
- 查询不存在的数据:攻击者可能发送大量无效请求。
- 频繁查询热门数据:数据频繁访问导致缓存未命中。
- 异常查询:数据服务故障导致缓存无法提供数据。
如何解决?
可以使用 布隆过滤器(Bloom Filter) 来拦截非法查询。
添加依赖
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>29.0-jre</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
创建布隆过滤器工具类
public class BloomFilterUtil {
private static final int expectedInsertions = 1000000; // 预计插入数量
private static final double fpp = 0.001; // 误判率
private static BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
expectedInsertions,
fpp
);
// 添加元素
public static void add(String key) {
bloomFilter.put(key);
}
// 判断元素是否存在
public static boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
}
在 Controller 中使用布隆过滤器
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/user/{id}")
public User getUserById(@PathVariable Long id) {
if (!BloomFilterUtil.mightContain(id.toString())) {
return null;
}
String userKey = "user_" + id;
User user = (User) redisTemplate.opsForValue().get(userKey);
if (user == null) {
user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(userKey, user, 300, TimeUnit.SECONDS);
} else {
BloomFilterUtil.add(id.toString());
}
}
return user;
}
缓存击穿
什么是缓存击穿?
缓存击穿指高并发访问下某个热点数据在缓存中不存在或过期,导致大量请求直接访问数据库。
如何解决?
通过加锁机制确保同一时间只有一个请求更新缓存。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
加锁机制实现
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@GetMapping("/user/{id}")
public User getUserById(@PathVariable Long id) {
String userKey = "user_" + id;
User user = (User) redisTemplate.opsForValue().get(userKey);
if (user == null) {
String lockKey = "lock_user_" + id;
String lockValue = UUID.randomUUID().toString();
try {
Boolean lockResult = redisTemplate.opsForValue().setIfAbsent(lockKey, lockValue, 60, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(lockResult)) {
user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(userKey, user, 300, TimeUnit.SECONDS);
}
}
} finally {
if (lockValue.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
return user;
}
缓存雪崩
什么是缓存雪崩?
缓存雪崩指大量缓存数据在同一时间失效,导致数据库压力骤增。
常见场景包括:
- 缓存服务器宕机。
- 大量数据同时过期。
- 数据访问波动大,如促销活动。
如何解决?
方法 1:分散过期时间
为不同数据设置随机化的过期时间。
方法 2:多级缓存架构
结合 Redis 和本地缓存(如 Ehcache)。
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
<version>2.10.6</version>
</dependency>
配置 Ehcache
application.properties
spring.cache.type=ehcache
CacheConfig
配置类
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public EhCacheCacheManager ehCacheCacheManager(CacheManager cm) {
return new EhCacheCacheManager(cm);
}
@Bean
public CacheManager ehCacheManager() {
EhCacheManagerFactoryBean cmfb = new EhCacheManagerFactoryBean();
cmfb.setConfigLocation(new ClassPathResource("ehcache.xml"));
cmfb.setShared(true);
return cmfb.getObject();
}
}
ehcache.xml
<ehcache>
<cache name="userCache" maxEntriesLocalHeap="10000"
timeToLiveSeconds="60" timeToIdleSeconds="30" />
</ehcache>
查询逻辑
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private CacheManager ehCacheManager;
@GetMapping("/user/{id}")
@Cacheable(value = "userCache", key = "#id")
public User getUserById(@PathVariable Long id) {
String userKey = "user_" + id;
User user = (User) ehCacheManager.getCache("userCache").get(userKey, User.class);
if (user == null) {
user = (User) redisTemplate.opsForValue().get(userKey);
if (user != null) {
ehCacheManager.getCache("userCache").put(userKey, user);
}
}
return user;
}
通过以上方法,可以有效解决缓存穿透、缓存击穿和缓存雪崩问题,提升系统的稳定性与性能。
评论区