返回顶部

Redis缓存相关问题(穿透、雪崩、击穿)及解决办法

首先,我们需要了解使用Redis缓存查询数据的流程是:
 1.数据查询首先进行缓存查询。
 2.如果数据存在则直接返回缓存数据。
 3.如果数据不存在,就对数据库进行查询,并把查询到的数据放进缓存。
 4.如果数据库查询数据为空,则不放进缓存。

//伪代码:
ServiceImpl.java
Public String getId(String cacheKey){
	String id = redisDao.get(cacheKey);//先查询redis缓存
	If(id == null){ //缓存数据不存在,查询数据库
		String iddb = idDao.find();//查询数据库
		If(iddb != null ){//判断数据库数据是否存在
			redisDao.set(cacheKey,iddb);//存在,存储到redis中
			return iddb;
}
}else{
	Retrun id; //缓存数据存在,直接返回数据
}
}

缓存穿透

概念:
缓存穿透是指一直查询缓存和数据库中都不存在的数据。比如id为-1的数据
现象:
在这里插入图片描述
解决方案:
1.数据校验
  1.1.先从redis查询数据,redis有,查询出来返回
  1.2.如果redis没有查询出数据,查询数据库,查询出来返回,并缓存到redis中
  1.3.如果数据库没有查询出数据,设置默认数据,存储到redis中,并设置有效期
  1.4.再次查询的时候,判断redis中获取的是否为默认值,是直接返回,不是则操作再返回。

//伪代码
public object GetProductListNew() {
    int cacheTime = 30;//缓存时间
    String cacheKey = "product_list";//缓存的key
	  String cacheValue = CacheHelper.Get(cacheKey);//获取redis数据
	  if(cacheValue != string.Empty){//判断redis中存储的是否为默认值
    	if (cacheValue != null) {//查询数据不为null
			return cacheValue;//返回查询出来的数据
		}else {//redis查询不出来数据
			//数据库查询数据
			cacheValue = GetProductListFromDB();
			if (cacheValue == null) {//数据查询数据库为null
				//如果发现为空,设置个默认值,也缓存起来
				cacheValue = string.Empty;
			}
			CacheHelper.Add(cacheKey, cacheValue, cacheTime);//把数据存储到redis
			return cacheValue;
		}
	  }else{
		return cacheValue;//返回默认值,方便调用者判断
	  }
}

2.布隆过滤器
https://www.jianshu.com/p/400dd82389b4?from=groupmessage

缓存雪崩

概念:
 缓存雪崩是指在某一个时间段,redis缓存的数据集中全部过期失效,在缓存集中失效的这个时
间段对数据的访问查询,都落到了数据库上,对于数据库而言,就会产生周期性的压力。
现象:
在这里插入图片描述

解决方案:
1.在批量往Redis存数据的时候,把每个数据的失效时间尽量都不一致,加锁排队或者加个随机值。
1.1.加锁排队

//伪代码
public object GetProductListNew() {
		    int cacheTime = 30;
		    String cacheKey = "product_list";
		    String lockKey = cacheKey;
		    String cacheValue = CacheHelper.get(cacheKey);
		    if (cacheValue != null) {
		        return cacheValue;
		    } else {
		        synchronized(lockKey) {
		            cacheValue = CacheHelper.get(cacheKey);
		            if (cacheValue != null) {
		                return cacheValue;
		            } else {
		              //这里一般是sql查询数据
		                cacheValue = GetProductListFromDB(); 
		                CacheHelper.Add(cacheKey, cacheValue, cacheTime);
		            }
		        }
		        return cacheValue;
		    }
		}

1.2.随机值:

//伪代码
public object GetProductListNew() {
	    int cacheTime = Random.nextInt(30);
	    String cacheKey = "product_list";
	    //缓存标记
	    String cacheSign = cacheKey + "_sign";
	    String sign = CacheHelper.Get(cacheSign);
	    //获取缓存值
	    String cacheValue = CacheHelper.Get(cacheKey);
	    if (sign != null) {
	        return cacheValue; //未过期,直接返回
	    } else {
	        CacheHelper.Add(cacheSign, "1", cacheTime);
	        ThreadPool.QueueUserWorkItem((arg) -> {
	      		//这里一般是 sql查询数据
	            cacheValue = GetProductListFromDB(); 
	          	//日期设缓存时间的2倍,用于脏读
	          	CacheHelper.Add(cacheKey, cacheValue, cacheTime * 2);                 
	        });
	        return cacheValue;
	    }
	} 

2.热门的数据(访问频率高的数据)可以缓存的时间长一些。
3.冷门的数据可以缓存的时间短一些。
4.特别热门的数据可以设置永不过期。

缓存击穿

概念:
 指一个Key非常热点,在不停的扛着大并发,大并发集中对这一个点进行访问,当这个Key在失效的瞬间,持续的大并发就穿破缓存,直接请求数据库,就像在一个完好无损的桶上凿开了一个洞。
现象 在这里插入图片描述
解决方案:
1.设置热点数据永远不过期。
2.设置互斥锁
 简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,
而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法。

//伪代码
public String get(key) {
	  	  String value = redis.get(key);//获取redis的数据
	      if (value == null) { //代表缓存值过期
	      		//设置3min的超时,防止del操作失败的时候,
	//下次缓存过期一直不能load db
	      		if (redis.setnx(key_mutex, 1, 3 * 60) == 1) {  //代表设置成功
	               	value = db.get(key);//数据库查询数据
	                redis.set(key, value, expire_secs);//redis设置数据
	                redis.del(key_mutex);//删除key_mutes
	        	} else {  
	//这个时候代表同时候的其他线程已经load db并回设到缓存了,
	//这时候重试获取缓存值即可
	                sleep(50);
	                get(key);  //重试
	           }
	      } else {
	          return value; //有数据直接返回     
	      }
	   }
posted @ 2020-09-08 22:20  洛水良遥  阅读(158)  评论(0编辑  收藏  举报