Redis中缓存击穿 缓存穿透 缓存雪崩解决方案
目录
1.缓存击穿
指的是单个key在缓存中查不到,去数据库查询,这样如果数据量不大或者并发不大的话是没有什么问题的
如果数据库数据量大并且是高并发的情况下那么就可能会造成数据库压力过大而崩溃
执行流程图
举个栗子
业务场景
假设有个获取文章接口,后台发布一篇新文章,文章为置顶推荐文章,这时有2000用户在同一秒钟同时访问这篇文章详情
如果这篇文章没有缓存,会导致数据压力过大!
代码如下
/**
* 获取文章详情 01-- 防止缓存击穿
*
* @date: 2020/12/2 14:10
* @return: Content
*/
@GetMapping("getContentDetail")
public Content getContentDetail(@RequestParam(value = "contentId") Long contentId) {
log.info("getContentDetail.req contentId={}", contentId);
Content content;
String detail = CONTENT + ":" + DETAIL + ":" + contentId;
String value = redisService.get(detail);
if (StringUtils.isNotEmpty(value)) {
log.info("从缓存获取数据.....");
return JSON.parseObject(value, Content.class);
}
content = getData(contentId);
// 查询文章内容不空设置缓存为10min
if (Objects.nonNull(content)) {
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);
}
return content;
}
/**
* 模拟数据库查询数据
*
* @param contentId 文章ID
* @date: 2020/12/30 10:50
* @return: com.zlp.entity.Content
*/
private Content getData(Long contentId) {
log.info("getData查询数据库获取数据count={}.....", count.incrementAndGet());
try {
Thread.sleep(40);
Optional<Content> con = contentList.stream().filter(content ->
content.getContentId().equals(contentId)).findFirst();
if (con.isPresent()) {
return con.get();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
构建一个contentList数据容器
/**
* 构建一个contentList数据容器
*/
private static List<Content> contentList = new ArrayList<>();
static {
contentList.add(new Content(1L, "说好不哭", "没有联络,后来的生活!", 1));
contentList.add(new Content(2L, "告别气球", "塞拉河畔,左岸的咖啡", 1));
contentList.add(new Content(3L, "等你下课", "你住的学校旁,我租了一间公寓", 1));
}
Jmeter测试如下
jmeter参数配置如下:
线程数:1000
循环次数:10
任务执行时间 :5
假设用1000用户,对这个接口进行访问,我们用AtomicInteger记录需要查询数据库次数!
控制台打印日志
结果
发现在同一时刻访问接口,设置缓存没有启动作用,怎样只能查询数据一次,其它到查询缓存数据,就会减轻数据库压力
这时我们可以添加分布式锁,只允许一个线程查询数据库,让其它线程自旋调用该方法,在查询缓存数据
代码如下
@GetMapping("getContentDetail02")
public Content getContentDetail02(@RequestParam(value = "contentId") Long contentId) {
log.info("getContentDetail.req contentId={}", contentId);
Content content;
String detail = CONTENT + ":" + DETAIL + ":" + contentId;
String conLock = CONTENT + ":" + LOCK + ":" + contentId;
String lock = "";
String value = redisService.get(detail);
if (StringUtils.isNotEmpty(value)) {
log.info("从缓存获取数据.....");
return JSON.parseObject(value, Content.class);
}
try {
lock = redisService.getLock(conLock, 10);
if (StringUtils.isNotEmpty(lock)) {
content = getData(contentId);
// 查询文章内容不空设置缓存为10min
if (Objects.nonNull(content)) {
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);
}
// 查询文章内容为空设置缓存为1min,避免缓存穿透
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 1);
return content;
}
// 休眠重新尝试调用方法
Thread.sleep(300);
getContentDetail(contentId);
} catch (Exception e) {
e.printStackTrace();
redisService.unLock(detail, lock);
} finally {
redisService.unLock(detail, lock);
}
return null;
}
2.缓存穿透
一般是出现这种情况是因为恶意频繁查询才会对系统造成很大的问题: key缓存并且数据库不存
在所以每次查询都会查询数据库从而导致数据库崩溃。
执行流程图
解决思路:
从DB中查询出来数据为空,也进行空数据的缓存,避免DB数据为空也每次都进行数据库查询,过期时间设置短一些
// 查询文章内容不空设置缓存为10min
if (Objects.nonNull(content)) {
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);
}
// 查询文章内容为空设置缓存为1min,避免缓存穿透
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 1);
使用布隆过滤器,但是会增加一定的复杂度及存在一定的误判率(判断不存在肯定是不存在,判断存在可能会不存在)
布隆过滤器原理:
布隆过滤器(Bloom Filter)的核心实现是一个超大的位数组和几个哈希函数。假设位数组的长度为m,哈希函数的个数为k
以上图为例,具体的操作流程:假设集合里面有3个元素{x, y, z},哈希函数的个数为3。首先将位数组进行初始化,将里面每个位都设置位0。对于集合里面的每一个元素,将元素依次通过3个哈希函数进行映射,每次映射都会产生一个哈希值,这个值对应位数组上面的一个点,然后将位数组对应的位置标记为1。查询W元素是否存在集合中的时候,同样的方法将W通过哈希映射到位数组上的3个点。如果3个点的其中有一个点不为1,则可以判断该元素一定不存在集合中。反之,如果3个点都为1,则该元素可能存在集合中。注意:此处不能判断该元素是否一定存在集合中,可能存在一定的误判率。可以从图中可以看到:假设某个元素通过映射对应下标为4,5,6这3个点。虽然这3个点都为1,但是很明显这3个点是不同元素经过哈希得到的位置,因此这种情况说明元素虽然不在集合中,也可能对应的都是1,这是误判率存在的原因。
布隆过滤器添加元素
- 将要添加的元素给k个哈希函数
- 得到对应于位数组上的k个位置
- 将这k个位置设为1
布隆过滤器查询元素
- 将要查询的元素给k个哈希函数
- 得到对应于位数组上的k个位置
- 如果k个位置有一个为0,则肯定不在集合中
- 如果k个位置全部为1,则可能在集合中
布隆过滤器实现
我们借助Redisson来实现布隆顾虑器
首先我们初始化布隆过滤器数据
/**
* 初始化布隆过滤器数据
*
* @date: 2020/12/30 13:34
* @return: void
*/
@GetMapping("initContentBloomData")
public void initContentBloomData() {
log.info("初始化布隆过滤器数据==>initContentBloomData...");
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter("CONTENT_PREFIX");
//初始化布隆过滤器,var1表示容量大小,var3表示容错率
bloomFilter.tryInit(1000L, 0.0001);
for (int i = 1; i < 1000; i++) {
bloomFilter.add(1);
}
log.info("CONTENT_PREFIX:1 是否存在:" + bloomFilter.contains(1));
log.info("CONTENT_PREFIX:2 是否存在:" + bloomFilter.contains(1002));
log.info("预计插入数量:" + bloomFilter.getExpectedInsertions());
log.info("容错率:" + bloomFilter.getFalseProbability());
log.info("hash函数的个数:" + bloomFilter.getHashIterations());
log.info("插入对象的个数:" + bloomFilter.count());
}
/**
* 获取文章详情 -- 防止缓存穿透
*
* @date: 2020/12/2 14:10
* @return: Content
*/
@GetMapping("getContentDetail03")
public Content getContentDetail03(@RequestParam(value = "contentId") Long contentId) {
log.info("getContentDetail.req contentId={}", contentId);
Content content;
String detail = CONTENT + ":" + DETAIL + ":" + contentId;
String conLock = CONTENT + ":" + LOCK + ":" + contentId;
RBloomFilter<Object> bloomFilter = redissonClient.getBloomFilter(BloomFilterConstants.CONTENT_PREFIX);
if (Boolean.FALSE.equals(bloomFilter.contains(contentId))) {
log.info("bloomFilter.contentId={},非法的文章ID", detail);
return null;
}
String lock = "";
String value = redisService.get(detail);
if (StringUtils.isNotEmpty(value)) {
log.info("从缓存获取数据.....");
return JSON.parseObject(value, Content.class);
}
try {
lock = redisService.getLock(conLock, 10);
if (StringUtils.isNotEmpty(lock)) {
content = getData(contentId);
// 查询文章内容不空设置缓存为10min
if (Objects.nonNull(content)) {
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 10);
}
// 查询文章内容为空设置缓存为1min,避免缓存穿透
redisService.setKeyByMINUTES(detail, JSON.toJSONString(content), 1);
return content;
}
// 休眠重新尝试调用方法
Thread.sleep(300);
getContentDetail(contentId);
} catch (Exception e) {
e.printStackTrace();
redisService.unLock(detail, lock);
} finally {
redisService.unLock(detail, lock);
}
return null;
}
测试
我们首先初始化数据,再调用getContentDetail03接口如下
我们做两次测试
第一次contentId=5
控制台输出
第二次contentId=1003
控制台输出
3.缓存雪崩
雪崩指的是多个key查询并且出现高并发,缓存中失效或者查不到,然后都去db查询,从而导致db压力突然飙升
从而崩溃。出现原因: 1 key同时失效, 2 redis本身崩溃了
解决办法
- 在缓存失效后,通过加锁或者队列来控制读数据库写缓存的线程数量。比如对某个key只允许一个线程查询数据和写缓存,其他线程等待。(跟击穿的第一个方案类似,但是这样是避免不了其它key去查数据库,只能减少查询的次数)
- 实现redis高可用
- 不同的key,设置不同的过期时间,具体值可以根据业务决定,让缓存失效的时间点尽量均匀
本文地址:https://blog.csdn.net/zouliping123456/article/details/111995888
上一篇: 微信小程序官方Demo部署测试
下一篇: 怎样才是有效的网站编辑工作
推荐阅读
-
【转载】在AspNetCore 中 使用Redis实现分布式缓存
-
浅谈Spring Boot中Redis缓存还能这么用
-
Redis之缓存雪崩、缓存穿透、缓存预热、缓存更新、缓存降级
-
[redis]关于 缓存穿透/缓存击穿/缓存雪崩 看这篇文章就够了
-
关于缓存穿透,缓存击穿,缓存雪崩,热点数据失效问题的解决方案(转)
-
Redis中几个简单的概念:缓存穿透/击穿/雪崩,别再被吓唬了
-
缓存穿透、缓存击穿、缓存雪崩概念及解决方案
-
vue spa应用中的路由缓存问题与解决方案
-
redis缓存穿透和缓存失效的预防和解决
-
高并发高可用复杂系统中的缓存架构(三) 能够支撑高并发 + 高可用 + 海量数据 + 备份恢复的 redis 的重要性