一、缓存穿透
缓存穿透是指用户查询数据,在数据库没有,自然在缓存中也不会有。这样就导致用户查询的时候,在缓存中找不到,每次都要去数据库再查询一遍,然后返回空(相当于进行了两次无用的查询)。这样请求就绕过缓存直接查数据库,这也是经常提的缓存命中率问题。
二、示意图
三、解决方案
说明:
在正常情况下,我们使用缓存的大概流程:
- 1.首先根据Key查询缓存
- 2.判断缓存内是否有值
- 2.1 有值则直接返回
- 2.2 没有则查询数据库,在存入缓存
从上面的流程我们大概就可以发现那里会出现问题,那就是在步骤2.2查询数据库如果也没有值,辞职赋值给缓存,跟定也是一个空,当下次在进行查询时,又会进行多次查询(查缓存,查数据库)
解决方案一
我们可以在第一步进行优化,对Key进行过滤处理,最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层存储系统的查询压力。
如果自己封装可能麻烦些,当然也有开源的,比如:Guava,Hutool等进行可封装!
以下是Guava BloomFilter的截取代码,有兴趣的可以进行研究
@Beta
public final class BloomFilter<T> implements Predicate<T>, Serializable {
private final BitArray bits;
private final int numHashFunctions;
private final Funnel<T> funnel;
private final BloomFilter.Strategy strategy;
.................
public BloomFilter<T> copy() {
return new BloomFilter(this.bits.copy(), this.numHashFunctions, this.funnel, this.strategy);
}
public boolean mightContain(T object) {
return this.strategy.mightContain(object, this.funnel, this.numHashFunctions, this.bits);
}
/** @deprecated */
@Deprecated
public boolean apply(T input) {
return this.mightContain(input);
}
public boolean put(T object) {
return this.strategy.put(object, this.funnel, this.numHashFunctions, this.bits);
}
public double expectedFpp() {
return Math.pow((double)this.bits.bitCount() / (double)this.bitSize(), (double)this.numHashFunctions);
}
}
解决方案二
我们可以在第二步进行优化,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),我们仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。通过这个直接设置的默认值存放到缓存,这样第二次到缓冲中获取就有值了,而不会继续访问数据库,这种办法最简单粗暴!
注意:使用此方法一定要设置过期时间,时间不应过长!
伪代码
public Object get(@RequestParam(value = "key") String key){
Object obj = redisUtil.get(key);
if(obj ==null){
System.out.println("这里进行数据库查询----get");
obj = logInfo.get(key);
if(obj == null)
obj = "Java有货";
redisUtil.set(key,obj, (long) (500+Math.random()* 100));
}
return obj;
}
四、缓存击穿
缓存击穿与缓存穿透有着本质的区别,缓存击穿是指,用户访问的热点key失效了,去访问数据库,在高并发时,给数据库带来的压力
缓存雪崩: 缓存大面积失效
缓存穿透:缓存内、数据库内都没有数据
缓存击穿:热点key失效,去访问数据库,数据库是有记录的
五、示意图
从示意图上看着和雪崩一样,但切记,击穿是热点Key的失效,热点Key必定是高并发的!后果可想而知!
六、解决方案
方案一
所谓热点key肯定是经常访问且访问量都是居高的,那么我自然也可将这些热点Key设置为永久的存在,这样就不担心他失效了
方案二
采用互斥锁Lock ,下面是伪代码
public Object get(@RequestParam(value = "key") String key){
Object obj = redisUtil.get(key);
try{
if(obj ==null){
System.out.println("这里进行数据库查询----get");
if(lock.tryLock()){//尝试获取锁
obj = logInfo.get(key);
if(obj == null)
obj = "Java有货";
redisUtil.set(key,obj, (long) (2+Math.random()* 10));
}else {//获取失败,休息100s,再次尝试
Thread.sleep(100);
obj = get(key);
}
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//释放锁
lock.unlock();
}
return obj;
}
关注 Java有货领取更多资料
联系小编。微信:372787553,带您进群互相学习
左侧小编微信,右侧获取免费资料
- Java 设计模式学习代码 https://github.com/Dylan-haiji/design-pattern
- SpringCloud学习代码: https://github.com/Dylan-haiji/javayh-cloud
- AlibabaCloud学习代码:https://github.com/Dylan-haiji/javayh-cloud-nacos
- SpringBoot+Mybatis 多数据源切换:https://github.com/Dylan-haiji/javayh-boot-data-soure
- Redis、Mongo、Rabbitmq、Kafka学习代码: https://github.com/Dylan-haiji/javayh-middleware
- SpringBoot+SpringSecurity实现自定义登录学习代码:https://github.com/Dylan-haiji/javayh-distribution