1.何为缓存穿透
缓存穿透 :缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。
比如查询一个id = 0的数据,这是在redis和数据库中肯定不存在的,这样就属于缓存穿透了。
常见的解决方案有两种:
-
缓存空对象
-
优点:实现简单,维护方便
-
缺点:
-
额外的内存消耗
-
可能造成短期的不一致
-
-
-
布隆过滤
-
优点:内存占用较少,没有多余key
-
缺点:
-
实现复杂
-
存在误判可能
-
-
由于布隆过滤器实现相对麻烦,现阶段先试用方案一:缓存空对象 来解决缓存穿透问题:
2.两种解决方案的思路
缓存空对象思路分析:当我们客户端访问不存在的数据时,先请求redis,但是此时redis中没有数据,此时会访问到数据库,但是数据库中也没有数据,这个数据穿透了缓存,直击数据库,我们都知道数据库能够承载的并发不如redis这么高,如果大量的请求同时过来访问这种不存在的数据,这些请求就都会访问到数据库。
简单的解决方案就是:哪怕这个数据在数据库中也不存在,我们也把这个数据存入到redis中去,这样,下次用户过来访问这个不存在的数据,那么在redis中也能找到这个数据,就不会进入到数据库了
布隆过滤:布隆过滤器其实采用的是哈希思想来解决这个问题,通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问redis,哪怕此时redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到redis中,
假设布隆过滤器判断这个数据不存在,则直接返回
这种方式优点在于节约内存空间,存在误判,误判原因在于:布隆过滤器走的是哈希思想,只要哈希思想,就可能存在哈希冲突
3.基于缓存空值解决缓存穿透问题
接下来我们就拿黑马点评项目做实例,解决商铺查询的缓存穿透问题:
核心思路如下:
在原来的逻辑中,我们如果发现这个数据在mysql中不存在,直接就返回404了,这样是会存在缓存穿透问题的
现在的逻辑中:如果这个数据不存在,我们不会返回404 ,还是会把这个数据写入到Redis中,并且将value设置为空,欧当再次发起查询时,我们如果发现命中之后,判断这个value是否是null,如果是null,则是之前写入的数据,证明是缓存穿透数据,如果不是,则直接返回数据。
我们修改ShopServiceImpl中queryById方法:
/**
* 根据id查询商铺
* @param id
* @return
*/
@Override
public Result queryById(Long id) {
//1.从redis查询商铺缓存
String key = RedisConstants.CACHE_SHOP_KEY + id;
String shopJson = stringRedisTemplate.opsForValue().get(key);
//2.判断是否存在
if(StrUtil.isNotBlank(shopJson)){
//3.存在,返回商铺信息
Shop shop = JSONUtil.toBean(shopJson, Shop.class);
return Result.ok(shop);
}
//经过上面的判断,只剩下 shopJson == null 和 shopJson == ""的情况
//判断命中的是否为空值
if(shopJson != null){
//只剩下 shopJson == ""的情况,正是我们缓存的空值
//返回错误信息
return Result.fail("店铺压根不存在!!!");
}
//4.不存在,去数据库查询
Shop shop = getById(id);
//5判断数据库中是否存在
if(shop == null){
//6.数据库中不存在,返回错误信息,并且缓存空数据
stringRedisTemplate.opsForValue().set(key,"",RedisConstants.CACHE_NULL_TTL,TimeUnit.MINUTES);
return Result.fail("店铺不存在!");
}
//7.数据库中存在,将商铺信息写入redis,返回商铺信息
stringRedisTemplate.opsForValue().set(key,JSONUtil.toJsonStr(shop));
//8.设置过期时间
stringRedisTemplate.expire(key,RedisConstants.CACHE_SHOP_TTL, TimeUnit.MINUTES);
return Result.ok(shop);
}
4.测试
接下来测试,看看效果是否符合预期:
我们重启服务,然后打开浏览器,访问一个不存在的商铺id = 0:
此时我们去redis控制台去看看,是否已经存入了空值
确实存入了空值。
那么这时候我们再去访问这个id == 0 的商铺,就是直接在redis中返回,而不是打到数据库中了,比如我们测试一下,再一次访问该接口:
可以看到errorMsg有所变化,是我们redis中获取到 " " 时,发出的提示
说明缓存空值确实实现了,那么就解决了缓存穿透的问题!
5.总结
5.1. 缓存穿透的原因
用户请求的数据在缓存中和数据库中都不存在,不断发起这样的请求,给数据库带来巨大压力,一般是受到了恶意攻击所导致的。
5.2.缓存穿透的解决方案
缓存穿透的解决方案有哪些?
-
缓存null值
-
布隆过滤
-
增强id的复杂度,避免被猜测id规律