🎓博主介绍:Java、Python、js全栈开发 “多面手”,精通多种编程语言和技术,痴迷于人工智能领域。秉持着对技术的热爱与执着,持续探索创新,愿在此分享交流和学习,与大家共进步。
📖DeepSeek-行业融合之万象视界(附实战案例详解100+)
📖全栈开发环境搭建运行攻略:多语言一站式指南(环境搭建+运行+调试+发布+保姆级详解)
👉感兴趣的可以先收藏起来,希望帮助更多的人
高并发场景下的 Spring Boot 优化:缓存穿透、雪崩与击穿解决方案
一、引言
在当今的互联网应用中,高并发场景是极为常见的,如电商的秒杀活动、社交媒体的热门话题等。Spring Boot 作为一个广泛使用的开发框架,在高并发场景下可能会面临各种性能挑战,其中缓存穿透、雪崩与击穿问题尤为突出。这些问题若不及时解决,会导致系统性能下降、数据库压力过大甚至系统崩溃。本文将详细介绍这些问题的成因,并给出相应的解决方案。
二、缓存穿透、雪崩与击穿问题概述
2.1 缓存穿透
缓存穿透是指查询一个一定不存在的数据,由于缓存中没有,所以会去查询数据库,但数据库中也没有该数据,这样每次请求都会穿透缓存直接打到数据库上,导致数据库压力过大。例如,在一个用户信息查询系统中,有人恶意请求一个不存在的用户 ID,就可能引发缓存穿透问题。
2.2 缓存雪崩
缓存雪崩是指在某一时刻,大量的缓存数据同时过期失效,导致大量的请求直接访问数据库,从而使数据库压力剧增,甚至可能导致数据库崩溃。这种情况通常是由于缓存服务器宕机或者大量缓存同时设置了相同的过期时间引起的。
2.3 缓存击穿
缓存击穿是指一个非常热门的 key,在缓存过期的瞬间,有大量的请求同时访问该 key,由于缓存中已经没有该 key 的数据,这些请求会全部打到数据库上,造成数据库瞬间压力过大。比如在电商的秒杀活动中,某个热门商品的缓存过期时,就可能出现缓存击穿问题。
三、Spring Boot 缓存基础
3.1 引入缓存依赖
在 Spring Boot 项目中,我们可以使用 Spring Cache 来实现缓存功能。首先,在 pom.xml 中添加相关依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
3.2 配置缓存
在 application.properties 中配置 Redis 连接信息:
spring.redis.host=127.0.0.1
spring.redis.port=6379
在启动类上添加 @EnableCaching 注解来启用缓存功能:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
3.3 使用缓存注解
在需要缓存的方法上添加 @Cacheable、@CachePut、@CacheEvict 等注解,例如:
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 从数据库中查询用户信息
return userRepository.findById(id).orElse(null);
}
}
四、缓存穿透解决方案
4.1 布隆过滤器
布隆过滤器是一种空间效率极高的概率型数据结构,用于判断一个元素是否存在于一个集合中。在缓存穿透问题中,我们可以使用布隆过滤器来过滤掉一定不存在的数据请求。以下是使用 Google Guava 库实现布隆过滤器的示例代码:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;
public class BloomFilterUtil {
private static final int EXPECTED_INSERTIONS = 1000000;
private static final double FALSE_POSITIVE_PROBABILITY = 0.01;
private static final BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.defaultCharset()),
EXPECTED_INSERTIONS,
FALSE_POSITIVE_PROBABILITY
);
public static void add(String key) {
bloomFilter.put(key);
}
public static boolean mightContain(String key) {
return bloomFilter.mightContain(key);
}
}
在业务代码中使用布隆过滤器:
@Service
public class UserService {
public User getUserById(Long id) {
String key = String.valueOf(id);
if (!BloomFilterUtil.mightContain(key)) {
return null;
}
// 从缓存中查询
User user = cache.get(key);
if (user == null) {
// 从数据库中查询
user = userRepository.findById(id).orElse(null);
if (user != null) {
cache.put(key, user);
}
}
return user;
}
}
4.2 缓存空对象
当查询一个不存在的数据时,我们可以将这个空结果也缓存起来,这样下次再请求相同的数据时,就可以直接从缓存中获取空结果,而不会再去查询数据库。示例代码如下:
@Service
public class UserService {
public User getUserById(Long id) {
String key = String.valueOf(id);
User user = cache.get(key);
if (user == null) {
// 从数据库中查询
user = userRepository.findById(id).orElse(null);
if (user == null) {
// 缓存空对象
cache.put(key, new User());
} else {
cache.put(key, user);
}
}
return user;
}
}
五、缓存雪崩解决方案
5.1 缓存过期时间随机化
为了避免大量缓存同时过期,我们可以为每个缓存设置一个随机的过期时间。示例代码如下:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
import java.util.Random;
@Service
public class CacheService {
private final RedisTemplate<String, Object> redisTemplate;
private final Random random = new Random();
public CacheService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public void setWithRandomExpire(String key, Object value, long minExpire, long maxExpire, TimeUnit timeUnit) {
long expire = minExpire + random.nextInt((int) (maxExpire - minExpire + 1));
redisTemplate.opsForValue().set(key, value, expire, timeUnit);
}
}
在业务代码中使用随机过期时间:
@Service
public class UserService {
private final CacheService cacheService;
public UserService(CacheService cacheService) {
this.cacheService = cacheService;
}
public User getUserById(Long id) {
String key = String.valueOf(id);
User user = (User) cacheService.get(key);
if (user == null) {
// 从数据库中查询
user = userRepository.findById(id).orElse(null);
if (user != null) {
// 设置随机过期时间
cacheService.setWithRandomExpire(key, user, 60, 120, TimeUnit.SECONDS);
}
}
return user;
}
}
5.2 多级缓存
使用多级缓存可以提高系统的可用性和性能。例如,我们可以同时使用本地缓存(如 Caffeine)和分布式缓存(如 Redis)。当请求到来时,先从本地缓存中查找,如果本地缓存中没有,再从分布式缓存中查找,最后再从数据库中查找。示例代码如下:
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
private final Cache<String, User> localCache = Caffeine.newBuilder()
.maximumSize(1000)
.expireAfterWrite(10, TimeUnit.MINUTES)
.build();
private final RedisTemplate<String, Object> redisTemplate;
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public User getUserById(Long id) {
String key = String.valueOf(id);
// 先从本地缓存中查找
User user = localCache.getIfPresent(key);
if (user == null) {
// 从分布式缓存中查找
user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
// 从数据库中查找
user = userRepository.findById(id).orElse(null);
if (user != null) {
// 更新分布式缓存
redisTemplate.opsForValue().set(key, user);
// 更新本地缓存
localCache.put(key, user);
}
} else {
// 更新本地缓存
localCache.put(key, user);
}
}
return user;
}
}
六、缓存击穿解决方案
6.1 互斥锁
使用互斥锁可以保证在缓存过期时,只有一个请求可以去查询数据库并更新缓存,其他请求等待缓存更新完成后再从缓存中获取数据。示例代码如下:
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
public class UserService {
private final RedisTemplate<String, Object> redisTemplate;
public UserService(RedisTemplate<String, Object> redisTemplate) {
this.redisTemplate = redisTemplate;
}
public User getUserById(Long id) {
String key = String.valueOf(id);
User user = (User) redisTemplate.opsForValue().get(key);
if (user == null) {
String lockKey = "lock:" + key;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (locked != null && locked) {
try {
// 从数据库中查询
user = userRepository.findById(id).orElse(null);
if (user != null) {
redisTemplate.opsForValue().set(key, user);
}
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 等待一段时间后重试
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return getUserById(id);
}
}
return user;
}
}
6.2 永不过期
对于一些热门的 key,我们可以设置为永不过期,然后在后台使用定时任务来更新缓存。示例代码如下:
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
private final CacheService cacheService;
private final UserRepository userRepository;
public UserService(CacheService cacheService, UserRepository userRepository) {
this.cacheService = cacheService;
this.userRepository = userRepository;
}
public User getUserById(Long id) {
String key = String.valueOf(id);
User user = (User) cacheService.get(key);
if (user == null) {
// 从数据库中查询
user = userRepository.findById(id).orElse(null);
if (user != null) {
// 设置永不过期
cacheService.set(key, user);
}
}
return user;
}
@Scheduled(fixedRate = 3600000) // 每小时更新一次缓存
public void updateHotUserCache() {
List<User> hotUsers = userRepository.findHotUsers();
for (User user : hotUsers) {
String key = String.valueOf(user.getId());
cacheService.set(key, user);
}
}
}
七、总结
在高并发场景下,缓存穿透、雪崩与击穿问题是 Spring Boot 应用中常见的性能挑战。通过合理使用布隆过滤器、缓存空对象、随机过期时间、多级缓存、互斥锁和永不过期等技术,我们可以有效地解决这些问题,提高系统的性能和稳定性。在实际应用中,我们需要根据具体的业务场景和需求选择合适的解决方案。


1599

被折叠的 条评论
为什么被折叠?



