缓存击穿问题
缓存击穿发生在访问热点数据,大量请求访问同一个热点数据,当热点数据失效后同时去请求数据库,瞬间耗尽数据库资源,导致数据库无法使用。
解决方案:
1、使用同步锁或分布式锁控制。
2、热点数据永不过期。
3、缓存预热,分为提前预热、定时预热
4、降级处理
1. 什么是缓存击穿
缓存击穿是指当大量的请求同时访问某个热点数据,而该数据在缓存中失效或到期的瞬间,所有请求同时绕过缓存直接请求数据库,导致数据库瞬间负载过高,严重时可能导致数据库宕机或服务崩溃。
缓存击穿通常发生在以下场景:
- 某个热点数据有大量的请求,而这个数据正好在缓存中失效。
- 缓存失效后,所有请求同时到达数据库,导致数据库压力瞬间暴增。
2. 解决方案
2.1 使用同步锁或分布式锁控制
为了解决缓存失效后多个线程同时访问数据库的问题,可以通过加锁来控制访问。在数据失效时,只有一个线程能去数据库查询,其他线程等待这个线程将数据加载到缓存后,再从缓存读取数据。常见的两种锁机制有:
(1)单体架构下(单进程内)可以使用同步锁控制查询数据库的代码,只允许有一个线程去查询数据库,查询得到数据库存入缓存。
synchronized(obj){
//查询数据库
//存入缓存
}
(2)分布式架构下(多个进程之间)可以使用分布式锁进行控制。分布式锁确保同一时刻只有一个实例可以去数据库查询,其他实例等待查询结果。
// 获取分布式锁对象
RLock lock = redisson.getLock("myLock");
try {
// 尝试加锁,最多等待100秒,加锁后自动解锁时间为30秒
boolean isLocked = lock.tryLock(100, 30, java.util.concurrent.TimeUnit.SECONDS);
if (isLocked) {
//查询数据库
//存入缓存
} else {
System.out.println("获取锁失败,可能有其他线程持有锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放锁
lock.unlock();
System.out.println("释放锁...");
}
2.2 热点数据永不过期
对于一些非常重要且访问量大的数据,可以通过设置这些热点数据永不过期的方式,避免其从缓存中失效。这样的话,缓存击穿的问题就不会发生。
redisTemplate.opsForValue().set("hotData", value, Duration.ofDays(Long.MAX_VALUE));
需要注意的是,虽然设置了永不过期,但还是要设计相应的后台机制来手动更新缓存中的数据,以保持数据的及时性。
2.3 缓存预热
缓存预热是指在系统启动或缓存失效之前,提前将热点数据加载到缓存中,避免缓存失效时发生大量请求同时访问数据库的情况。缓存预热可以通过以下两种方式实现:
(1)提前预热:在系统上线前,提前将已知的热点数据加载到缓存中。通常可以通过后台程序或者脚本,预先将这些热点数据从数据库中查询出来并缓存。
@PostConstruct
public void preheatCache() {
List<User> hotUsers = database.getHotUsers(); // 获取热点数据
for (User user : hotUsers) {
cache.put(user.getId(), user); // 预热缓存
}
}
(2)定时预热:对于某些数据可以设置定时任务,在缓存即将到期时,自动刷新缓存,避免缓存过期导致的击穿。
@Scheduled(fixedDelay = 60000) // 每隔一分钟执行一次
public void refreshCache() {
List<User> hotUsers = database.getHotUsers();
for (User user : hotUsers) {
cache.put(user.getId(), user); // 更新缓存
}
}
2.4 降级处理
当缓存失效并且数据库负载过高,系统可选择进行降级处理,避免继续增加数据库压力。这种策略可以通过以下方式实现:
-
返回默认值:当缓存和数据库都不可用时,返回一些默认的静态数据,或者告知用户系统繁忙稍后再试,避免对数据库造成更大压力。
-
限流:在缓存失效时,对某些重要的接口进行限流,只允许一部分请求通过,其余的请求返回降级响应。这样可以保护数据库不被大量请求压垮。
@Service
public class UserService {
private RateLimiter rateLimiter = RateLimiter.create(10); // 每秒最多允许10个请求
public User getUserById(Long id) {
if (!rateLimiter.tryAcquire()) {
return new User(); // 降级,返回默认值
}
User user = cache.get(id);
if (user == null) {
user = database.getUserById(id);
cache.put(id, user);
}
return user;
}
}