缓存击穿的解决方案
这个主要是最近准备八股文缓存专题时做的笔记,主要是对于缓存击穿的解决方案,目前总结起来大概三种 分别是缓存预热、分布式锁以及永不过期,这三种这里做了详细的讲述,看看到过年前能不能把缓存专题给整完,fighting!
缓存击穿是指在高并发系统中,一个热点数据缓存的过期或者缓存中的热点数据不存在,导致大量并发请求直接访问数据库,从而给数据库造成巨大压力,甚至宕机的现象。 说具体点就是当某个热点数据在缓存中过期了,如果此时有大量并发请求同时访问这个数据,由于缓存中这个数据不存在,其所有的请求都会直接访问数据库,从而导致数据库压力急剧增加。
一般来说,解决缓存击穿的方法主要有三种:
- 加分布式锁
- 热点数据预加载(缓存预热)
- 热点数据永不过期
这里我们来逐步进行分析,分析如何通过互斥锁的方式来解决缓存击穿问题、
1、查询缓存不存在查询数据库
这个就是第一版的解决方案,也是我们对于普通数据的缓存方案,这个方案首先查询缓存中对应的数据是否存在,如果不存在就请求数据库,数据库中存在就将当前数据写入到缓存中,这个方案的问题也比较明显:如果缓存中热点数据的缓存过期或者被删除,大量的请求将会打到数据库,从而导致数据库压力较大。
伪代码如下:
public String queryInfo(String id){
// 1. 从缓存中获取对应的数据
String cacheData = cache.get(id);
// 2.判断获取的数据是否为空
if(cacheData == null){
// 查询缓存数据为空,,查询数据库
String dbData = db.queryById(id);
if(dbData != null){
cache.set(id,data);
cacheData = dbData;
}
}
// 返回数据
return cacheData;
}
2、通过分布式互斥锁的方案降低数据库压力
分布式锁的解决方案就是保证只有一个请求可以访问数据库,其他需要访问数据库的请求等待结果。这样的话可以避免大量的请求同时访问数据库。
这个主要是在原有的基础上进行一定的调整,即限制对于数据库的请求,每次只有一个请求可以访问数据库,这种方案可以有效地避免缓存击穿问题,因为只有一个线程可以在同一时间内查询数据库,其他线程则需要等待,这样的话就不会同时穿透到数据库。 其主要在原有的基础上进行修改,伪代码如下:
public String queryInfo(String id){
// 1. 从缓存中获取对应的数据
String cacheData = cache.get(id);
// 2.判断获取的数据是否为空
if(cacheData == null){
// 查询缓存数据为空,,查询数据库
Lock lock = getLock(id);
lock.lock();
try{
String dbData = db.queryById(id);
if(dbData != null){
cache.set(id,data);
cacheData = dbData;
}
}finally{
lock.unlock();
}
}
// 返回数据
return cacheData;
}
但是这样有一个弊端,那就是获取分布式锁的请求,都会执行一次查询数据库的请求,然后将查询结果更新到缓存中去。但是从理论上来说,只有第一次加载数据库记录的请求是有效的。 这种情况会导致以下两个问题的发生:
- 海量的用户获取锁之后都查询数据库,这样的话会造成数据库性能的浪费,因为如此多的请求当中,只有第一次的查询请求是有效的,其他的查询请求都是可以避免的。
- 查询数据库过多的话可能会导致用户响应时间变长,接口吞吐量下降等情况。
所以,针对以上情况,可以采用双重判定锁的方式来进行优化。 双重判定锁的逻辑很简单,如下图所示,在查询数据库前,再次查询一下缓存,查看缓存中是否存在对应的数据,如果存在,则直接返回,不存在才去查询数据库。
根据以上情况,其伪代码如下:
public String queryInfo(String id){
// 1. 从缓存中获取对应的数据
String cacheData = cache.get(id);
// 2.判断获取的数据是否为空
if(cacheData == null){
// 查询缓存数据为空,,查询数据库
Lock lock = getLock(id);
lock.lock();
try{
// 进行缓存数据的二次判断
cacheData = cache.get(id);
if(cacheData == null){
// 缓存数据为空
String dbData = db.queryById(id);
if(dbData != null){
cache.set(id,data);
cacheData = dbData;
}
}
}finally{
lock.unlock();
}
}
// 返回数据
return cacheData;
}
总结一下双重判断锁的逻辑
- 获取锁:在查询数据库之前,先尝试获取一个分布式锁,只有一个线程可以成功获取锁,其他线程等待。
- 查询数据库:如果双重判断锁确认缓存中不存在对应数据,那么执行查询数据库操作,获取数据。
- 将数据写入缓存:获取到数据之后,将数据写入缓存,并且设置一个合适的过期时间,以防止缓存永远不会被更新
- 释放锁:最后,释放获取的锁。使得其他线程可以继续使用这个锁。
热点数据预加载,在活动开始之前,针对已知的热点数据从数据库加载到缓存中,这样的话可以避免海量的请求第一次访问热点数据的时候需要从数据库读取的流程,降低数据库的压力。可以极大地减少请求的响应时间,有效避免缓存击穿。
热点数据永不过期,指的是将可以预知的热点数据,在活动开始之前,将过期时间设置为 -1,这样的话就不会有缓存击穿的风险了。
这个可以搭配热点数据预加载的方案一起实现,等到热点数据的时间结束之后,其数据的访问量降低了,通天阁后台任务的方案针对缓存设置过期时间,从而降低 Redis 存储的压力。