Redis 缓存雪崩
缓存雪崩是指缓存中大量的 key 同时过期,导致大量的请求直接打到数据库上,从而压垮数据库。这种情况通常发生在系统启动时或者缓存预热时,大量的 key 同时过期,导致系统性能急剧下降。
1. 缓存雪崩的原因
大量 key 同时过期:在系统启动时或者缓存预热时,大量的 key 同时过期,导致大量的请求直接打到数据库上。
Redis 服务宕机:如果 Redis 服务宕机,所有的请求都会直接打到数据库上。
缓存预热不当:在系统启动时,如果没有合理的缓存预热策略,可能会导致大量的 key 同时过期。
2. 缓存雪崩的解决方案
2.1 设置不同的过期时间
为了避免大量的 key 同时过期,可以给不同的 key 设置不同的过期时间,比如在基础过期时间上加上一个随机值。
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
public void cacheUser(User user) {
String key = "user:" + user.getId();
// 基础过期时间:1小时
int baseExpire = 3600;
// 随机过期时间:0-300秒
int randomExpire = new Random().nextInt(300);
// 最终过期时间:1小时 + 随机时间
int finalExpire = baseExpire + randomExpire;
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), finalExpire, TimeUnit.SECONDS);
}
}
2.2 使用互斥锁
当缓存未命中时,使用互斥锁确保只有一个请求去查询数据库,其他请求等待。
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
public User getUserById(Long id) {
String key = "user:" + id;
// 1. 先从缓存中查询
String userJson = redisTemplate.opsForValue().get(key);
if (userJson != null) {
return JSON.parseObject(userJson, User.class);
}
// 2. 缓存未命中,尝试获取锁
String lockKey = "lock:" + key;
Boolean locked = redisTemplate.opsForValue().setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
if (Boolean.TRUE.equals(locked)) {
try {
// 3. 获取锁成功,查询数据库
User user = userMapper.selectById(id);
if (user != null) {
// 4. 将结果写入缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 3600, TimeUnit.SECONDS);
}
return user;
} finally {
// 5. 释放锁
redisTemplate.delete(lockKey);
}
} else {
// 6. 获取锁失败,等待一段时间后重试
try {
Thread.sleep(100);
return getUserById(id);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
}
}
}
}
2.3 使用缓存降级
当缓存服务不可用时,可以降级为本地缓存或者直接返回默认值。
@Service
public class UserService {
@Autowired
private StringRedisTemplate redisTemplate;
// 本地缓存
private final Map<Long, User> localCache = new ConcurrentHashMap<>();
public User getUserById(Long id) {
// 1. 先从本地缓存中查询
User user = localCache.get(id);
if (user != null) {
return user;
}
try {
// 2. 本地缓存未命中,查询Redis
String key = "user:" + id;
String userJson = redisTemplate.opsForValue().get(key);
if (userJson != null) {
user = JSON.parseObject(userJson, User.class);
// 更新本地缓存
localCache.put(id, user);
return user;
}
// 3. Redis未命中,查询数据库
user = userMapper.selectById(id);
if (user != null) {
// 更新Redis和本地缓存
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), 3600, TimeUnit.SECONDS);
localCache.put(id, user);
}
return user;
} catch (Exception e) {
// 4. Redis服务异常,降级为本地缓存
return localCache.getOrDefault(id, new User());
}
}
}
2.4 使用缓存预热
在系统启动时,提前将热点数据加载到缓存中。
@Service
public class CacheWarmUpService {
@Autowired
private StringRedisTemplate redisTemplate;
@Autowired
private UserMapper userMapper;
@PostConstruct
public void warmUp() {
// 1. 获取热点数据
List<User> hotUsers = userMapper.getHotUsers();
// 2. 将热点数据写入缓存
for (User user : hotUsers) {
String key = "user:" + user.getId();
// 设置不同的过期时间
int baseExpire = 3600;
int randomExpire = new Random().nextInt(300);
int finalExpire = baseExpire + randomExpire;
redisTemplate.opsForValue().set(key, JSON.toJSONString(user), finalExpire, TimeUnit.SECONDS);
}
}
}
3. 最佳实践
合理设置过期时间:避免大量的 key 同时过期,可以给不同的 key 设置不同的过期时间。
使用互斥锁:当缓存未命中时,使用互斥锁确保只有一个请求去查询数据库。
使用缓存降级:当缓存服务不可用时,可以降级为本地缓存或者直接返回默认值。
使用缓存预热:在系统启动时,提前将热点数据加载到缓存中。
监控缓存状态:实时监控缓存的状态,及时发现和处理缓存雪崩问题。
使用多级缓存:可以使用多级缓存来减少缓存雪崩的影响,比如本地缓存 + Redis 缓存。
使用限流措施:当检测到缓存雪崩时,可以使用限流措施来保护数据库。
4. 注意事项
- 互斥锁方案要注意死锁问题
- 本地缓存要注意内存占用
- 缓存预热要注意预热时机
- 降级策略要考虑数据一致性
- 限流措施要考虑业务影响
