缓存穿透
缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,如发起为id为小于0的数据或id为特别大等不存在的数据。这时的用户很可能是攻击者,攻击会导致数据库压力过大。举例代码:
/**
* 通过分类id查询广告集合
* @param categoryId 广告分类id
* @return 广告集合
*/
public List<Content> findContentById(Long contentCatId){
// 首先根据分类id到Redis中去获取数据 - Constants.CONTENT_LIST_REDIS为Key
List<Content> contentList = (List<Content>) redisTemplate.boundHashOps(Constants.CONTENT_LIST_REDIS).get(contentCatId);
if(contentList == null){
// 如果没有,就去数据库查询
List<Content> contentList = contentDao.findContentById(categoryId);
// 然后再存一份到Redis
redisTemplate.boundHashOps(Constants.CONTENT_LIST_REDIS).put(contentCatId,contentList);
}
return contentList;
}
上面代码就存在缓存穿透的问题,如果用户查询的分类id为-8,Redis中不存在这个数据,那么它就会去MySQL数据库中查询。MySQL里面也不存在这个数据,所以返回的结果是null。然后把这个null以key-value存入到Redis中。即Redis中存一个null值,Redis是不会存储null值的。因此,下一次从Redis中查询依旧是null值,则根据代码逻辑又会去查MySQL,形成一个无限循环,就会导致MySQL压力巨大,这就是缓存穿透问题。
解决方案:
①在接口层增加校验,如对用户进行鉴权校验(是否能访问这个接口),对查询的id做基础校验,如id<=0,直接进行拦截。但这种方法遗漏一个问题,那就是id值过大(大到数据库中依旧不存在),我们没办法对id值过大进行校验。
②在上面方法进行改进,当MySQL中也查询不到的时候,我们就往Redis中存一个不为null的值,这样可以防止攻击用户反复用同一个id进行暴力攻击。这种方法依旧存在问题,那就是如果攻击用户每次攻击的id递减或是递增,每次的id不同,还是会去访问MySQL,然后还会使Redis中存储的数据飞涨。
/**
* 通过分类id查询广告集合
* @param categoryId 广告分类id
* @return 广告集合
*/
public List<Content> findContentById(Long contentCatId){
// 首先根据分类id到Redis中去获取数据
List<Content> contentList = (List<Content>) redisTemplate.boundHashOps("content_list").get(contentCatId);
if(contentList == null){
// 如果没有,就去数据库查询
contentList = contentDao.findContentById(categoryId);
if(contentLIST == null){
// 如果MySQL中依旧不存在,则创建一个空的集合
contentList = new ArrayList<>();
}
// 然后再存一份到Redis
redisTemplate.boundHashOps("content_list").put(contentCatId,contentList);
}
return contentList;
}
③缓存预热,缓存预热就是将数据提前加入到缓存中,当数据发生变更,再将最新的数据更新到缓存。即我们在Redis中查询不到的时候也不再访问MySQL数据库去查询了,Redis中没有,那么MySQL中必定也没有。
缓存击穿
缓存击穿是指缓存中没有但数据库中有的数据。这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力。
缓存穿透和缓存击穿的区别:
1、一般缓存穿透是由恶意攻击造成的,而缓存击穿则是由Redis中不存在数据,数据库中存在,且在某一时刻并发量较大造成。
2、一般缓存穿透的方法都会传一个id之类的值过来,而缓存击穿的方法不会传参数过来。
举例代码:
/**
* 查询所有菜单集合(导航栏数据)
* @return 菜单集合
*/
public List<String> findCatTreeName(){
// 从Redis中查询所有菜单名
List<String> catTreeList = redisTemplate.boundValueOps("categoryTree").get();
// 如果查询结果为null
if(catTreeList == null){
// 从数据库中查询
catTreeList = categoryDao.findAllCatTree();
// 然后存入Redis,并设置一个过期时间60天
redisTemplate.boundValueOps("categoryTree").set(catTreeList,60,TimeUnit.DAYS);
}
return catTreeList;
}
上面代码就存在缓存击穿问题,而这个缓存击穿问题往往就是因为过期时间造成的。当Redis中存的数据过期之后,在某一刻,10000个人同时访问这个方法,由于Redis中不存在数据,则这10000个人都会去访问MySQL数据库,导致数据库压力巨大。
解决方案:
1、设置数据永不过期
2、缓存预热
缓存雪崩
缓存雪崩是指缓存数据大批量到过期时间,而查询数据量巨大,引起数据库压力过大甚至宕机。和缓存击穿不同的是,缓存击穿指并发查同一条数据,缓存雪崩是不同数据都过期了,很多数据都查不到从而查数据库。
解决方案:
1、缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
2、设置数据永不过期
3、缓存预热