一、缓存击穿
1. 定义
缓存击穿是指一个热点 key(访问非常频繁的 key),在它的缓存过期的瞬间,大量的请求同时访问这个 key,这些请求发现缓存过期后,会同时去数据库查询该数据,导致数据库压力瞬间增大
2. 解决方案
(1)设置热点数据永不过期
对于一些极端热点数据,我们可以将其缓存设置为永不过期。不过这种方法可能会导致缓存中的数据和数据库中的数据不一致的情况随着时间积累而加重,需要谨慎使用。例如,对于一个电商系统中某些爆款商品的库存信息,如果这些商品的热度极高,而且库存变化不是非常频繁,可以考虑将其缓存设置为永不过期。
(2)使用互斥锁
当缓存失效时,使用互斥锁来保证只有一个线程去查询数据库并更新缓存,其他线程等待。比如在 Java 中,可以使用ReentrantLock来实现互斥锁。以下是一个简单的示例代码:
import java.util.concurrent.locks.ReentrantLock;
public class CacheBreakdownSolution {
private static final ReentrantLock lock = new ReentrantLock();
public Object getValueFromCache(String key) {
Object value = getFromCache(key);
if (value == null) {
lock.lock();
try {
value = getFromCache(key);
if (value == null) {
value = queryFromDB(key);
setToCache(key, value);
}
} finally {
lock.unlock();
}
}
return value;
}
// 从缓存中获取数据的方法
private Object getFromCache(String key) {
// 这里是具体从缓存获取数据的逻辑,暂不详细实现
return null;
}
// 从数据库查询数据的方法
private Object queryFromDB(String key) {
// 这里是具体从数据库查询数据的逻辑,暂不详细实现
return null;
}
// 将数据存入缓存的方法
private void setToCache(String key, Object value) {
// 这里是具体将数据存入缓存的逻辑,暂不详细实现
}
}
在上述代码中,getValueFromCache方法用于获取缓存数据。首先尝试从缓存获取,如果不存在,则加锁后再次尝试获取(防止多个线程同时进入加锁代码块),如果还是没有,则从数据库查询并更新缓存,最后释放锁。
(3) 提前缓存预热
原理:在系统启动或者业务低峰期,主动将可能会被频繁访问的热点数据加载到缓存中。这样,在真正的高流量访问到来时,这些热点数据已经在缓存中,并且有足够的时间来更新缓存的过期时间等信息,减少缓存击穿的可能性。
示例:对于一个在线教育平台,在每天课程开始前,可以将热门课程的信息(如课程大纲、讲师介绍等)提前加载到缓存中。可以通过编写一个定时任务或者在系统初始化阶段,使用脚本或者专门的缓存预热工具来实现。假设使用 Spring 框架,通过@Scheduled注解可以方便地实现定时预热缓存的功能。代码示例(简化版):
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.List;
@Component
public class CachePreheat {
private final CourseService courseService;
private final CacheService cacheService;
public CachePreheat(CourseService courseService, CacheService cacheService) {
this.courseService = courseService;
this.cacheService = cacheService;
}
@Scheduled(cron = "0 0 0 * * *") // 每天凌晨执行
public void preheatCache() {
List<Course> hotCourses = courseService.getHotCourses();
for (Course course : hotCourses) {
cacheService.put("course:" + course.getId(), course);
}
}
}
在上述代码中,CachePreheat类通过@Scheduled注解在每天凌晨执行preheatCache方法。该方法从CourseService获取热门课程列表,然后将这些课程信息存入缓存(通过CacheService)。
二、缓存穿透
1. 定义
缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中的,请求每次都会穿透缓存直接查询数据库,这样就可能导致数据库压力过大。例如,攻击者恶意构造大量不存在的查询请求,就会对数据库造成严重的负载冲击。
(1)缓存空对象
当从数据库查询不到数据时,将空对象(比如null或者一个自定义的表示空的数据结构)缓存起来,并且设置一个较短的过期时间。这样下次有相同的请求时,直接从缓存中获取空对象,就不会再穿透到数据库了。但是这种方法可能会导致缓存中存储大量的空对象,浪费缓存空间。
(2)使用限流组件
在应用层面对请求进行限流,当检测到大量疑似缓存穿透的请求时,限制这些请求访问后端数据库的频率。这样可以防止恶意攻击者通过大量不存在的请求压垮数据库。例如,通过设置每秒允许通过的请求数量上限,当请求超过这个上限时,直接返回一个提示信息(如 “请求过于频繁,请稍后再试”)。
(3)使用布隆过滤器
布隆过滤器是一种概率型数据结构,它可以高效地判断一个元素是否在一个集合中。在缓存系统中,在查询数据库之前,先使用布隆过滤器判断请求的 key 是否可能存在于数据库中。如果布隆过滤器判断不存在,那么直接返回,不会去查询数据库。布隆过滤器可能会存在一定的误判率(即实际上存在的数据可能被判断为不存在),但是可以通过合理调整参数来降低误判率。假设我们使用 Guava 库中的布隆过滤器,以下是一个简单的示例:
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import java.nio.charset.Charset;
public class CachePenetrationSolution {
private static final int EXPECTED_INSERTIONS = 1000;
private static final double FPP = 0.01;
private static BloomFilter<String> bloomFilter = BloomFilter.create(
Funnels.stringFunnel(Charset.forName("UTF-8")), EXPECTED_INSERTIONS, FPP);
static {
// 假设这里是预先将数据库中存在的key放入布隆过滤器的过程
// 这里可以从数据库中获取所有可能的key,并放入布隆过滤器
String[] existingKeys = {"key1", "key2", "key3"};
for (String key : existingKeys) {
bloomFilter.put(key);
}
}
public Object getValue(String key) {
if (!bloomFilter.mightContain(key)) {
return null;
}
Object value = getFromCache(key);
if (value == null) {
value = queryFromDB(key);
if (value == null) {
// 可以选择缓存空对象,此处暂不实现
} else {
setToCache(key, value);
}
}
return value;
}
// 从缓存中获取数据的方法、从数据库查询数据的方法、将数据存入缓存的方法
// 与缓存击穿部分类似,暂不详细实现
}
在上述代码中,首先定义了一个布隆过滤器bloomFilter,并预先将一些数据库中存在的 key 放入其中。getValue方法在查询数据时,先通过布隆过滤器判断 key 是否可能存在,如果不存在则直接返回null,避免查询数据库;如果可能存在,则再按照正常的缓存查询流程进行操作。
三、缓存雪崩
1. 定义
缓存雪崩是指在某一时刻,大量的缓存 key 同时过期,或者缓存服务器出现故障,导致大量的请求直接访问数据库,从而使数据库压力骤增,甚至可能导致数据库崩溃。
2. 解决方案
(1)设置缓存过期时间随机化
避免大量的缓存 key 在同一时间过期,在设置缓存过期时间时,给每个 key 加上一个随机的时间偏移量。例如,如果原本缓存过期时间是 1 小时,可以设置一个在 50 - 70 分钟之间的随机过期时间。这样就可以分散缓存过期的时间点,减少缓存雪崩的风险。
(2)多级缓存
采用多级缓存架构,比如使用本地缓存(如Guava Cache)和分布式缓存(如Redis)相结合。当分布式缓存出现问题或者大量数据过期时,本地缓存可以暂时承担一部分请求,减轻数据库的压力。以一个新闻网站为例,新闻详情页的缓存可以先在本地缓存中查找,如果没有再去分布式缓存中查找,最后才查询数据库。
(3)服务熔断和降级
当缓存雪崩发生,数据库压力过大时,启动服务熔断机制,暂时停止对部分非核心功能的服务,只保证核心功能的正常运行。例如,在一个电商系统中,当发生缓存雪崩时,可以暂停商品推荐功能(非核心功能),只保证用户下单、支付等核心功能的正常服务。同时,可以对服务进行降级处理,返回一个简化版的数据或者提示信息,比如只返回商品的基本信息而不是完整的商品详情页。
以上各解决方案只列举出一部分,有兴趣的同学自行搜索查找。



5万+

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



