最近小生遇到了好多人问缓存击穿的问题,作为一个乐于学习的渣渣,小生当然要去网上大肆搜集一番,综合小生的经验整理出了本篇文章,希望可以帮助到大家。
要想研究缓存击穿就得先知道什么是缓存?所谓缓存是Web应用开发同时也是互联网分层架构中非常重要的一个部分,通常用来缓解数据库的压力,从而提升系统整体性能,缩短用户的访问时间。
Web应用获取数据时先会去cache中找数据
所谓缓存击穿就是web应用去请求一个不在cache中或者已经失效的数据时,就会触发一次数据库查询,如果瞬间有大量类似的操作时就会导致数据库压力过大,造成反应过慢甚至宕机瘫痪。
综上所述产生缓存击穿的场景主要两种
场景一:数据不存在
方案一:
缓存null:当通过某个key去查询时,如果cache和数据库都不存在对应的数据,那么cache可以将这个key对应的value设置为一个默认值(比如:null),并且设置一个缓存过期的时间,这样在缓存失效这段时间内,所有访问这个key的请求都会被cache拦截,等到缓存失效后再去数据库中查询。
缺点:如果不存在的数据非常多,则会存储大量的缓存垃圾,浪费资源。
方案二:
Bloomfilter:类似于一种哈希表算法,在cache之前预判这个key是否合法,如果不合法则直接返回。
缺点:存在误杀的可能性
方案三:
屏蔽IP:记录并分析访问不存在的数据请求,获取其IP地址,如果在规定时间内此IP超过某个设定值,就将它加入黑名单,在系统中进行拦截。
缺点:只针对一些黑客攻击
场景二:数据失效
方案一:
后台刷新:后台启动一个常驻进程,用于在缓存失效前主动更新缓存中的数据,在缓存失效前后台进程刷新一下缓存中的数据,保证数据永远不会过期。
缺点:会增加系统难度,比较适合那些key相对固定,cache力度较大的业务。若是key比较分散则不太合适,实现起来也比较复杂。
方案二:
get请求主动更新缓存:将缓存key的过期时间点一起保存到缓存里(这个方法有很多,拼接,添加新字段,单独key皆可),在每次执行get操作后,都将get出来的缓存过期时间和当前系统时间做一次对比,如果缓存时间减去当前系统时间小于等于某个设定值后,则主动更新缓存。这样就能保证缓存中的数据始终是最新的。
缺点:在某个特殊时刻时,例如在缓存即将过期时没有get请求,导致缓存已经过期,恰好此时有大量并发请求过来,那就惨了。当然这种情况比较极端,但也是有可能的。
方案三:
分级缓存:采用L1(一级缓存)和L2(二级缓存)的缓存方式,L1缓存失效时间短,L2缓存失效时间长。请求优先从L1获取数据,如果L1未命中则加锁,只有1个线程获取到锁,这个线程再从数据库中读取数据并将数据更新到L1和L2中,而其他线程依旧从L2缓存中获取数据并返回。
缺点:这种方式主要是通过避免缓存同时失效并结合锁机制实现。那么就会出现一个问题,有一瞬间L2可能会存在脏数据。就是当数据更新时,L1的缓存被淘汰,但是L2未被淘汰。且这种方案可能会造成额外的缓存空间浪费。
方案四:
加锁:简单粗暴,而且方法也很多。网上随随便便就可以查到,小生对锁的研究不是很深,所以就在网上筛选出了一个比较好点的方案,话不多说,上代码!!!
static Lock reenLock = new ReentrantLock();
public List getData04() throws InterruptedException {
List result = new ArrayList();
// 从缓存读取数据
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 getData04();// 重试
}
}
}
return result;
}
小生的分享就到这里啦,祝各位大佬工作愉快,步步高升!