文章目录
缓存击穿
什么时候Redis中没有要查询的数据呢?答案是过期和新增:过期:在Redis会有一个key值,每个key值都有一个ttl,也就是生命周期,一旦过期了就只能存在数据库里了;新增:插入,更新的数据还未来得及同步到Redis中。
如果线程查询一个数据库中不存在的值,此时从数据库中就返回一个空值。当一个线程不断执行同一条查询语句查询这个Redis和数据库都不存在的数据时,比如说执行上万次同一条查询语句,那么每次都穿过Redis,这样Redis就没有意义了。那么为了解决这个问题,就可以在Redis中设置空值,同一条查询语句对应的key(数据库中没有的值)都对应着一个空值,当查询其他查询语句也查询数据库中不存在的值时,都要对每个key设置一个空值,随着越来越多这样的key,Redis服务器就会承受不了这么大的压力,例如下面的例子就引出了Redis缓存穿透:
缓存击穿: 一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。
缓存击穿如何解决
加锁更新
查询缓存,发现缓存中不存在,加锁,让其它线程等待,只让一个线程去更新缓存。
该方法是比较普遍的做法,即,在根据key获得的value值为空时,先锁上,再从数据库加载,加载完毕,释放锁。若其他线程发现获取锁失败,则睡眠50ms后重试。
至于锁的类型,单机环境用并发包的Lock类型就行,集群环境则使用分布式锁( redis的setnx)
单机伪代码
static Lock reenLock = new ReentrantLock();
//最后使用互斥锁的方式来实现.
public List<String> getData() throws InterruptedException {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getDataFromCache();
if (result.isEmpty()) {
if (reenLock.tryLock()) {
try {
System.out.println("我拿到锁,从DB获取数据库后写入缓存");
// 从数据库查询数据
result = getDataFromDB();
// 将查询到的数据写入缓存
setDataToCache(result);
} finally {
reenLock.unlock();// 释放锁
}
} else {//我没有拿到锁
result = getDataFromCache();// 先查一下缓存
if (result.isEmpty()) {
System.out.println("我没拿到锁,缓存也没数据,等一下");
Thread.sleep(100);//
return getData();// 递归调用重试
}
}
}
return result;
}
集群环境的redis的代码如下所示:
String get(String key) {
String value = redis.get(key);
if (value == null) {
if (redis.setnx(key_mutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(key_mutex, 3 * 60)
value = db.get(key);
redis.set(key, value);
redis.delete(key_mutex);
} else {
//其他线程休息50毫秒后重试
Thread.sleep(50);
get(key);
}
}
}
优点
思路简单
保证一致性
缺点
代码复杂度增大
存在死锁的风险
异步构建缓存
在这种方案下,构建缓存采取异步策略,会从线程池中取线程来异步构建缓存,从而不会让所有的请求直接怼到数据库上。该方案redis自己维护一个timeout,当timeout小于System.currentTimeMillis()时,则进行缓存更新,否则直接返回value值。
集群环境的redis代码如下所示:
String get(final String key) {
V v = redis.get(key);
String value = v.getValue();
long timeout = v.getTimeout();
if (v.timeout <= System.currentTimeMillis()) {
// 异步更新后台异常执行
threadPool.execute(new Runnable() {
public void run() {
String keyMutex = "mutex:" + key;
if (redis.setnx(keyMutex, "1")) {
// 3 min timeout to avoid mutex holder crash
redis.expire(keyMutex, 3 * 60);
String dbValue = db.get(key);
redis.set(key, dbValue);
redis.delete(keyMutex);
}
}
});
}
return value;
}
优点
性价最佳,用户无需等待
缺点
无法保证缓存一致性
布隆过滤器
布隆过滤器的巨大用处就是,能够迅速判断一个元素是否在一个集合中。因此他有如下三个使用场景:
网页爬虫对URL的去重,避免爬取相同的URL地址
反垃圾邮件,从数十亿个垃圾邮件列表中判断某邮箱是否垃圾邮箱(同理,垃圾短信)
缓存击穿,将已存在的缓存放到布隆过滤器中,当黑客访问不存在的缓存时迅速返回避免缓存及DB挂掉。
OK,接下来我们来谈谈布隆过滤器的原理
其内部维护一个全为0的bit数组,需要说明的是,布隆过滤器有一个误判率的概念,误判率越低,则数组越长,所占空间越大。误判率越高则数组越小,所占的空间越小。
private static BloomFilter bloomFilter =BloomFilter.create(Funnels.integerFunnel(), size);
String get(String key) {
String value = redis.get(key);
if (value == null) {
if(!bloomfilter.mightContain(key)){
return null;
}else{
value = db.get(key);
redis.set(key, value);
}
}
return value;
}
缓存穿透
什么是缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为“-1”的数据或id为特别大不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。
与上面讲到的缓存击穿的区别:
缓存击穿是指缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力
缓存穿透可能有两种原因:
自身业务代码问题
恶意攻击,爬虫造成空命中
解决方案
缓存空值/默认值
一种方式是在数据库不命中之后,把一个空对象或者默认值保存到缓存,之后再访问这个数据,就会从缓存中获取,这样就保护了数据库。
缓存空值有两大问题:
空值做了缓存,意味着缓存层中存了更多的键,需要更多的内存空间(如果是攻击,问题更严重),比较有效的
方法是针对这类数据设置一个较短的过期时间,让其自动剔除。
缓存层和存储层的数据会有一段时间窗口的不一致,可能会对业务有一定影响。
例如过期时间设置为5分钟,如果此时存储层添加了这个数据,那此段时间就会出现缓存层和存储层数据的不一致。
这时候可以利用消息队列或者其它异步方式清理缓存中的空对象。
布隆过滤器
除了缓存空对象,我们还可以在存储和缓存之前,加一个布隆过滤器,做一层过滤。
布隆过滤器里会保存数据是否存在,如果判断数据不不能再,就不会访问存储。
缓存雪崩
简单说:由于缓存不可用,导致大量请求访问后端服务,可能 mysql 扛不住高并发而打死, 像滚雪球一样,影响越来越大,最后导致整个网站崩溃不可用
至于为什么会像滚雪球一样?整个与整个系统的架构有关;
redis 集群彻底崩溃:不可用
缓存服务在请求 redis 时,会有大量的线程阻塞,占用资源
超时请求失败之后,会去 mysql 查询原始数据,mysql 抗不住,被打死
源头服务由于 mysql 被打死,对源服务的请求也被阻塞,占用资源
缓存服务大量的资源全部耗费在访问 redis 和 源服务上;最后自己被拖死,无法提供服务
nginx 无法访问缓存服务,只能基于本地缓存提供服务,当缓存过期后,就耗费在访问缓存服务上
最后整个网站崩溃,页面加载不出来任何数据
考虑的比较完善的一套方案,分为事前、事中、事后三个层次去思考再怎么来应对缓存雪崩的场景
事前解决方案
发生缓存雪崩之前,事情之前,怎么去避免 redis 彻底挂掉
redis本身的高可用性、复制、主从架构,操作主节点,读写,数据同步到从节点,一旦主节点挂掉,从节点跟上
双机房部署,一套 redis cluster,部分机器在一个机房,另一部分机器在另外一个机房
还有一种部署方式,两套 redis cluster,两套 redis cluster 之间做一个数据的同步,redis 集群是可以搭建成树状的结构的
一旦说单个机房出了故障,至少说另外一个机房还能有些 redis 实例提供服务
事中解决方案
redis cluster 已经彻底崩溃了,已经开始大量的访问无法访问到 redis 了
ehcache 本地缓存
所做的多级缓存架构的作用上了 ,ehcache 的缓存应对零散的 redis 中数据被清除掉的现象,另外一个主要是预防 redis 彻底崩溃
多台机器上部署的缓存服务实例的内存中,还有一套 ehcache 的缓存,还能支撑一阵
对 redis 访问的资源隔离
对 redis 访问使用 hystrix 进行隔离,防止自己资源大量阻塞在访问 redis 上
对源服务访问的限流以及资源隔离
同上,防止自己资源大量阻塞在访问源服务上,同时 hystrix 在资源隔离时也做到了限流
事后解决方案
redis 数据可以恢复,之前讲解过各种备份机制,redis 数据备份和恢复,redis 重新启动起来
redis 数据彻底丢失了或者数据过旧,快速缓存预热,redis 重新启动起来
由于事中做了限流与隔离,缓存服务不会被打死,通过熔断策略 和 half-open 策略, 可以自动可以恢复对 redis 的访问,发现 redis 可以访问了,就自动恢复了
熔断降级
服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。