缓存概念
在计算器中,高速缓冲存储器是一个硬件或软件组件,其存储数据,以便该数据可以在将来的请求送达更快;存储在缓存中的数据可能是早期计算的结果,也可能是存储在其他位置的数据的副本。一个缓存命中时,所请求的数据在高速缓存中找到,而出现高速缓存未命中当它不能发生时发生。缓存命中是通过从缓存中读取数据来实现的,这比重新计算结果或从速度较慢的数据存储中读取要快。因此,从缓存中可以处理的请求越多,系统执行速度就越快。
缓存穿透
缓存穿透的概念很简单,用户想要查询一个数据,发现redis内存数据库没有,也就是缓存没有命中,于是向持久层数据库查询。发现也没有,于是本次查询失败。当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库。这会给持久层数据库造成很大的压力,这时候就相当于出现了缓存穿透。
想象一下这个情况,如果传入的参数为-1,会是怎么样?这个-1,就是一定不存在的对象。就会每次都去查询数据库,而每次查询都是空,每次又都不会进行缓存。假如有恶意攻击,就可以利用这个漏洞,对数据库造成压力,甚至压垮数据库。即便是采用UUID,也是很容易找到一个不存在的KEY,进行攻击。
(这里注意的区别:缓存穿透的概念与缓存击穿并不相同,下面会列出缓存击穿的概念)
缓存穿透如何去预防呢?
1)布隆过滤器
布隆过滤器是一种数据结构,垃圾网站和正常网站加起来全世界据统计也有几十亿个。网警要过滤这些垃圾网站,总不能到数据库里面一个一个去比较吧,这就可以使用布隆过滤器。假设我们存储一亿个垃圾网站地址。
可以先有一亿个二进制比特,然后网警用八个不同的随机数产生器(F1,F2, …,F8) 产生八个信息指纹(f1, f2, …, f8)。接下来用一个随机数产生器 G 把这八个信息指纹映射到 1 到1亿中的八个自然数 g1, g2, …,g8。最后把这八个位置的二进制全部设置为一。过程如下:
那这个布隆过滤器是如何解决redis中的缓存穿透呢?很简单首先也是对所有可能查询的参数以hash形式存储,当用户想要查询的时候,使用布隆过滤器发现不在集合中,就直接丢弃,不再进行缓存查询。
2、缓存空对象
当存储层不命中后,即使返回的空对象也将其缓存起来,同时会设置一个过期时间,之后再访问这个数据将会从缓存中获取,保护了后端数据源;
这种方法会存在两个问题(占用空间资源较多、业务一致性问题):
Object nullValue = new Object();
try {
Object valueFromDB = getFromDB(uid); //从数据库中查询数据
if (valueFromDB == null) {
cache.set(uid, nullValue, 10); //如果从数据库中查询到空值,就把空值写入缓存,设置较短的超时时间
} else {
cache.set(uid, valueFromDB, 1000);
}
} catch(Exception e) {
cache.set(uid, nullValue, 10);
}
1.如果空值能够被缓存起来,这就意味着缓存需要更多的空间存储更多的键,因为这当中可能会有很多的空值的键;
即使对空值设置了过期时间,还是会存在缓存层和存储层的数据会有一段时间窗口的不一致,这对于需要保持一致性的业务会有影响。(这个方法需要具体根据业务分析方案可行性。)
缓存击穿
缓存击穿,是指一个key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个屏障上凿开了一个洞。 缓存被“击穿”的问题,(这个和缓存雪崩的区别在于这里针对某一key缓存,前者则是很多key,这里是指同一个key。)
缓存击穿的问题又该如何去预防呢?
- 数据持久化。设置热点数据永远不过期。(这个基于业务去考虑是否热点数据是可以持久化的操作。)
- 检查更新。将缓存key的过期时间(缓存数据存入时写的过期)一起保存到缓存中.在每次获取缓存数据操作后,都将get出来的缓存过期时间与当前系统时间做一个对比,如果缓存过期时间-当前系统时间<=1分钟(自定义的一个值),则主动更新缓存.这样就能保证缓存中的数据始终是最新的(和方案一一样,让数据不过期.)
- 使用互斥锁。
互斥锁的实现思路是什么呢?就是当某一热点数据失效后,通过互斥锁的机制把所有并发实现一个串行化的管理。只有拿到锁的线程,才去数据库查询数据更新缓存,其他的线程在缓存中没有拿到数据且没有拿倒锁的状态下进行等待。这样很大程度下降低了对DB数据库的压力。不好的地方是可以当缓存失效时。会有短暂的等待时间。下面上一个互斥锁的demo。
// 方法1:
static Lock reenLock = new ReentrantLock();
public List<String> getData() throws Exception {
List<String> result = new ArrayList<String>();
// 从缓存读取数据
result = getCacheData();
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 getData04();// 重试
}
}
}
return result;
}
缓存雪崩
缓存雪崩概念是指:缓存层出现了错误,不能正常工作了。大量key同一时间点失效,同时又有大量请求打进来,导致流量直接打在DB上,造成DB也会CPU爆满挂掉的情况。
那么如何解决缓存雪崩问题呢?
redisTemplate.opsForValue().set("2","xxx",2, 随机数);
解决思路1:可以给缓存的失效时间都设置成随机值,或者分析用户行为。利用程序手段尽量时缓存key值不在同一时间失效。从而减少DB的瞬间压力(根据具体业务分析)。
解决思路2:设置多级缓存。比如同时使用redis和memcache缓存,请求->redis->memcache->db;这样可以方式redis缓存同一时间,并发请求不会直接打到DB问题,而是memcache缓存中那去数据。
今天的分享到这里结束了。看完的小伙伴点个赞再走呗。