目录
1. 缓存穿透
问题描述
缓存穿透是指查询一个不存在的数据,由于缓存中没有该数据,请求会直接打到数据库上,导致数据库压力过大。
解决方案
- 缓存空值:即使查询的数据不存在,也将空值缓存起来,并设置一个较短的过期时间。
- 布隆过滤器:在查询缓存之前,先通过布隆过滤器判断数据是否存在。
示例代码
java
@Service
public class UserService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public User getUserById(Long id) {
String key = "user:" + id;
// 从缓存中获取数据
User user = (User) redisTemplate.opsForValue().get(key);
if (user != null) {
return user;
}
// 缓存中不存在,查询数据库
user = userRepository.findById(id).orElse(null);
if (user == null) {
// 缓存空值,设置较短的过期时间
redisTemplate.opsForValue().set(key, null, 60, TimeUnit.SECONDS);
} else {
// 缓存查询结果
redisTemplate.opsForValue().set(key, user, 1, TimeUnit.HOURS);
}
return user;
}
}
2. 缓存雪崩
问题描述
缓存雪崩是指大量缓存数据在同一时间失效,导致所有请求都打到数据库上,造成数据库压力过大甚至崩溃。
解决方案
- 设置不同的过期时间:为缓存数据设置随机的过期时间,避免大量缓存同时失效。
- 使用分布式锁:在缓存失效时,使用分布式锁保证只有一个线程去加载数据。
示例代码
java
@Service
public class ProductService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
@Autowired
private RedissonClient redissonClient;
public Product getProductById(Long id) {
String key = "product:" + id;
Product product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 使用分布式锁防止缓存击穿
RLock lock = redissonClient.getLock("lock:" + key);
try {
lock.lock();
// 双重检查,防止其他线程已经加载了数据
product = (Product) redisTemplate.opsForValue().get(key);
if (product != null) {
return product;
}
// 查询数据库
product = productRepository.findById(id).orElse(null);
if (product != null) {
// 设置随机的过期时间
int expireTime = 3600 + new Random().nextInt(600); // 1小时 + 随机10分钟
redisTemplate.opsForValue().set(key, product, expireTime, TimeUnit.SECONDS);
}
} finally {
lock.unlock();
}
return product;
}
}
3. 缓存击穿
问题描述
缓存击穿是指某个热点数据在缓存中失效后,大量请求同时打到数据库上,导致数据库压力过大。
解决方案
- 使用互斥锁:在缓存失效时,使用互斥锁保证只有一个线程去加载数据。
- 永不过期策略:对热点数据设置永不过期,通过后台任务定期更新缓存。
示例代码
java
@Service
public class HotDataService {
@Autowired
private RedisTemplate<String, Object> redisTemplate;
public String getHotData() {
String key = "hot_data";
String data = (String) redisTemplate.opsForValue().get(key);
if (data != null) {
return data;
}
// 使用 Redis 的 SETNX 实现互斥锁
String lockKey = "lock:" + key;
boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "locked", 10, TimeUnit.SECONDS);
if (locked) {
try {
// 双重检查
data = (String) redisTemplate.opsForValue().get(key);
if (data != null) {
return data;
}
// 模拟从数据库加载热点数据
data = loadHotDataFromDB();
redisTemplate.opsForValue().set(key, data, 1, TimeUnit.HOURS);
} finally {
// 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 未获取到锁,等待重试
try {
Thread.sleep(100);
return getHotData(); // 重试
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
return data;
}
private String loadHotDataFromDB() {
// 模拟数据库查询
return "hot_data_from_db";
}
}
4. Redis 连接池耗尽
问题描述
在高并发场景下,Redis 连接池可能会被耗尽,导致请求失败。
解决方案
- 增加连接池大小:根据实际需求调整连接池的最大连接数。
- 优化连接使用:确保每次操作 Redis 后及时释放连接。
示例代码
在 application.yml
中配置连接池:
yaml
spring:
redis:
host: localhost
port: 6379
lettuce:
pool:
max-active: 50 # 最大连接数
max-idle: 10 # 最大空闲连接数
min-idle: 5 # 最小空闲连接数
5. Redis 序列化问题
问题描述
默认情况下,Spring Boot 使用 JdkSerializationRedisSerializer 进行序列化,可能导致存储的数据不易阅读或兼容性问题。
解决方案
使用更高效的序列化方式,如 Jackson2JsonRedisSerializer
或 StringRedisSerializer
。
示例代码
java
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> template = new RedisTemplate<>();
template.setConnectionFactory(factory);
// 使用 Jackson2JsonRedisSerializer 序列化值
Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class);
template.setValueSerializer(serializer);
template.setHashValueSerializer(serializer);
// 使用 StringRedisSerializer 序列化键
template.setKeySerializer(new StringRedisSerializer());
template.setHashKeySerializer(new StringRedisSerializer());
return template;
}
}
6. 总结
在 Spring Boot 项目中使用 Redis 时,可能会遇到缓存穿透、缓存雪崩、缓存击穿、连接池耗尽以及序列化等问题。通过合理的缓存策略、分布式锁、连接池配置和序列化方式,可以有效解决这些问题,提升系统的稳定性和性能。希望本文的解决方案和示例代码能帮助到你!