通过redis构建缓存时,会出现如下几个问题:
缓存穿透
缓存穿透是指查询一个根本不存在的数据(缓存和数据库都没有),先查缓存,缓存没有,再去数据库中拿,而数据库中也没有,这样的请求如果不停地接收,那系统存储层会造成巨大压力,失去了缓存保护后端存储的意义。
出现缓存穿透的基本原因可能是自身业务代码或者数据出现问题,以及一些恶意攻击。
问题解决
1.缓存空对象
伪代码
取数据时先查找缓存,有则返回,无则取数据库
String get(String key){
//从缓存中取数据
String cacheV = cache.get(key);
//缓存为空
if(stringUtils.isBlank(cacheV)){
//从数据库中取,放入缓存
String dbV = db.get(key);
cache.set(key,dbV);
//数据库是null ,设置过期时间
if(dbV == null){
cache.expire(key,60*6);
}
return dbV;
}else{
return cacheV;
}
}
2.布隆过滤器
关于布隆过滤器我在上篇文章中说到了,它能过滤一定不存在,因为有hash冲突,如果一个key通过几次hash运算刚好对应的位值为1,则判断为存在,所以它的存在指的是可能存在,这种概率极低。
//初始化布隆过滤器
RBloomFilter<String> bloomFilter = redisson.getBloomFilter("names");
//初始化布隆过滤器:预计元素为100000000L,误差率为3%
bloomFilter.tryInit(100000000L,0.03);
//把所有数据存入布隆过滤器
void init(){
for (String key: keys) {
bloomFilter.put(key);
}
}
String get(String key) {
// 从布隆过滤器这一级缓存判断下key是否存在
Boolean exist = bloomFilter.contains(key);
if(!exist){
return "";
}
// 从缓存中获取数据
String cacheV = cache.get(key);
// 缓存为空
if (stringUtils.isBlank(cacheV)) {
// 从存储中获取
String dbV = db.get(key);
cache.set(key, dbV);
// 这里就是解决刚才说的那种极低事件的发生
if (dbV == null) {
cache.expire(key, 60 * 6);
}
return dbV;
} else {
// 缓存非空
return cacheV;
}
}
缓存击穿
缓存击穿指的是少量的热点key在不停的被访问,这是突然失效了,大量请求到数据库层,就像是在缓存层有了一个洞。
解决方案: 设置热点key 永不过期,或者使用互斥锁、分布式锁重建缓存
String get(String key){
//从缓存中取数据
String cacheV = cache.get(key);
//缓存为空
if(stringUtils.isBlank(cacheV)){
//让第一个请求进来
String mutexKey = "m:key:" + key;
if (redis.set(mutexKey, "2", "ex 180", "nx")) {
// 从数据源获取数据
value = db.get(key);
// 回写Redis, 并设置过期时间
redis.setex(key, timeout, value);
// 删除key_mutex
redis.delete(mutexKey);
}
else {// 其他线程休息500毫秒后重试
Thread.sleep(500);
get(key);
}
}
return cacheV;
}
缓存雪崩
缓存雪崩指缓存层支撑不起大量的流量导致宕机了,造成缓存层无效,崩塌,流量打到数据库。
解决方案:
保证缓存层的高可用性,比如redis 哨兵或集群模式。
使用上面的方式进行热点数据缓存重建,或热点数据随机过期。
使用降级熔断组件。