一、缓存穿透
缓存穿透就是redis没有该数据,数据库也没有该数据,外部不断的请求就会穿透redis,对底层数据库造成压力。
解决的根本在与,判断出redis和数据库不可能有的数据,将这个数据拦截在请求redis之前。
解决办法是利用布隆过滤器:
1、在redis安装布隆过滤器模块
2、在添加数据的时候,用springboot-redis的api,计算出值的hash值,将这些值对应的下标在布隆数组里面设置成1,这样就可以保证之后可以判断值是否必定不存在了。因为后面请求的值hash之后,如果计算出的下标里面没有一个为1的,就代表肯定不存在。
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
redisTemplate.opsForValue().setBit(key, i, true);
}
3、在查询数据的时候,在查询redis之前,先查询布隆过滤器
int[] offset = bloomFilterHelper.murmurHashOffset(value);
for (int i : offset) {
if (!redisTemplate.opsForValue().getBit(key, i)) {
return false;
}
}
按照hash之后的结果,查询对应下标是否为1,如果都不为1,则代表这个值之前肯定是没有的,那就是说redis和数据库也都没有,是无效数据,没有必要再去访问redis和数据库了。
二、缓存击穿
缓存击穿就是某些热点数据,先查询redis,redis有就直接返回,redis无或者有效时间到了就再去访问数据库,
如果大量的线程在redis失效期间访问redis,就会击穿redis到达数据库层,对数据库造成压力。
解决的根本在于,当感知到这个key在redis里面已经失效的时候,要保证只能有一个线程去请求数据库,
其它的线程等待,等请求数据库的线程返回了数据,就将数据刷到redis里面,
其它线程就可以在redis里面获取到数据了。
这样就保证了redis缓存失效的时候,只有一个线程去请求数据库,对数据库的压力减小,
但是这样做也有缺点,就是大量的请求轮询等等,会造成阻塞.
所以还有一种除了互斥锁的其它办法,就是定时任务刷新key的有效期,或者热点数据直接设置为永久。
解决办法:
利用redis的setNx来做互斥锁,setNx是当set的值没有时才执行成功并返回1,
那么当第一个线程setNx返回成功之后,其它线程就不能再setNx返回成功了,这样既达到了互斥锁的目的。
public String get(key) {
String value = redis.get(key);
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.del(key_mutex);
} else { //这个时候代表同时候的其他线程已经load db并回设到缓存了,这时候重试获取缓存值即可
sleep(50);
get(key); //重试
}
} else {
return value;
}
}
判断如果key对应的缓存失效,则先用setnx设置互斥锁,保证只有一个线程去请求db,
请求到了就刷到redis里面,并删除掉这个互斥锁。其它线程这个时候就可以就可以之间从redis里面获取到数据了。
三、缓存雪崩
缓存雪崩就是大批量的key一起失效,大量的请求全部击穿redis,直接访问数据库造成数据库崩溃。
想对应缓存击穿,只不过一个是对应单个key失效的情况,一个是大批量key同时失效的情况。
解决的根本在于,不让大批量的redis数据同一时间失效,可以将redis失效时间加上一个随机值,
还有加上一个缓存标记,
真实数据的失效时间是这个缓存标记的两倍,当缓存标记失效之后,
马上更新缓存标记和真实数据的失效时间。
这样就可以保证真实数据的失效时间永远不能到因为前面有个缓存标记保护着。
解决办法:
public object GetProductListNew() {
int cacheTime = 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倍。