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; //有数据直接返回
}
}