【案例实战】SpringBoot整合Redis实现缓存分页数据查询

正式观看本文之前,设想一个问题,高并发情况下,首页列表数据怎么做?

在这里插入图片描述

类似淘宝首页,这些商品是从数据库中查出来的吗?答案肯定不是,在高并发的情况下,数据库是扛不住的,那么我们要怎么去扛住C端大并发量呢,这块我们可以借助Redis,我们知道Redis是一个基于内存的NoSQL数据库。学过操作系统我们都知道,内存要比磁盘的效率大的多,那Redis就是基于内存的,而数据库是基于磁盘的。

还有类似天猫聚划算商品类表。
在这里插入图片描述

我们现在知道要用Redis去做首页数据的分页,那么我们应该用Redis的那种数据结构来做呢。

Redis有5种基本的数据结构,我们这里用list类型做分页。

在 Redis 中,List(列表)类型是按照元素的插入顺序排序的字符串列表。你可以在列表的头部(左边)或者尾部(右部)添加新的元素。

ok,那么接下来我们就通过一个案例实操一下,首页热点数据怎么放到Redis中去查询。

SpringBoot整合RedisTemplate这里就不做过多介绍啦,大家可以网上找篇博文 整合一下。

<!-- 创建SpringBoot项目加入redis的starter依赖 -->
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

编写ProductService,定于数据分页方法。

public interface ProductService {Map<String,Object> productListPage(int current, int size) throws InterruptedException;}

编写ProductServiceImpl实现类。

/*** @author lixiang* @date 2023/6/18 21:01*/
@Service
@Slf4j
public class ProductServiceImpl implements ProductService {private static final String PRODUCT_LIST_KEY = "product:list";private static final List<Product> PRODUCT_LIST;//模拟从数据库中查出来的数据static {PRODUCT_LIST = new ArrayList<>();for (int i = 1; i <= 100; i++) {Product product = new Product();product.setId(UUID.randomUUID().toString().replace("-", ""));product.setName("商品名称:" + i);product.setDesc("商品描述:" + i);product.setPrice(new BigDecimal(i));product.setInventory(2);PRODUCT_LIST.add(product);}}@Autowiredprivate RedisTemplate redisTemplate;@Overridepublic Map<String, Object> productListPage(int current, int size) throws InterruptedException {//从缓存中拿到分页数据List<Product> productList = getProductListByRedis(current, size);if (productList == null || productList.size() == 0) {log.info("当前缓存中无分页数据,当前页:" + current + ",页大小:" + size);//从数据库中拿到分页数据productList = getProductListByDataSource(current, size);}Map<String, Object> resultMap = new HashMap<>();//计算当前总页数int totalPage = (PRODUCT_LIST.size() + size - 1) / size;resultMap.put("total", PRODUCT_LIST.size());resultMap.put("data", productList);resultMap.put("pages", totalPage);return resultMap;}private List<Product> getProductListByRedis(int current, int size) {log.info("从Redis取出商品信息列表,当前页:" + current + ",页大小:" + size);// 计算总页数int pages = pages(size);// 起始位置int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);// 终止位置int end = start+size-1;List<Product> list = redisTemplate.opsForList().range(PRODUCT_LIST_KEY, start, end);List<Product> productList = list;return productList;}/*** 获取商品信息集合** @return*/private List<Product> getProductListByDataSource(int current, int size) throws InterruptedException {//模拟从DB查询需要300msThread.sleep(300);log.info("从数据库取出商品信息列表,当前页:" + current + ",页大小:" + size);// 计算总页数int pages = pages(size);// 起始位置int start = current <= 0 ? 0 : (current > pages ? (pages - 1) * size : (current - 1) * size);//数据缓存到redis中redisTemplate.opsForList().rightPushAll(PRODUCT_LIST_KEY, PRODUCT_LIST);//设置当前key过期时间为1个小时redisTemplate.expire(PRODUCT_LIST_KEY,1000*60*60, TimeUnit.MILLISECONDS);return PRODUCT_LIST.stream().skip(start).limit(size).collect(Collectors.toList());}/***  获取总页数* @param size* @return*/private Integer pages(int size){int pages = PRODUCT_LIST.size() % size == 0 ? PRODUCT_LIST.size() / size : PRODUCT_LIST.size() / size + 1;return pages;}
}

ok,然后编写controller,进行测试。

@RestController
@RequestMapping("/api/v1/product")
public class ProductController {@Autowiredprivate ProductService productService;@GetMapping("/page")public Map<String,Object> page(@RequestParam("current") int current,@RequestParam("size") int size){Map<String, Object> stringObjectMap;try {stringObjectMap = productService.productListPage(current, size);} catch (InterruptedException e) {stringObjectMap = new HashMap<>();}return stringObjectMap;}
}

当第一次访问的时候,先去Redis中查询,发现没有,然后就去查DB,将要缓存的数据页放到Redis中。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

第二次访问的时候。就直接访问Redis啦

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

通过Redis和DB查询的对比,我们发现从Redis中拿出来只用了18ms,从公DB中需要300ms,由此可见Redis的一个强大之处。

那么我们观察一下查询逻辑,会不会有什么问题。

    public Map<String, Object> productListPage(int current, int size) throws InterruptedException {//从缓存中拿到分页数据List<Product> productList = getProductListByRedis(current, size);if (productList == null || productList.size() == 0) {log.info("当前缓存中无分页数据,当前页:" + current + ",页大小:" + size);//从数据库中拿到分页数据productList = getProductListByDataSource(current, size);}}

设想,假如某一时刻,Redis中的缓存失效啦,大量的请求,全部查到DB上,也会带来一个灾难。所以这快又涉及到一个缓存击穿的问题。

解决缓存击穿

  • 方案一:永不过期
    • 提前把热点数据不设置过期时间,后台异步更新缓存。
  • 方案二:加互斥锁或队列
    • 其实我理解缓存击穿和缓存穿透差不多,所以加一个互斥锁,让一个线程正常请求数据库,其他线程等待即可(这里可以使用线程池来处理),都创建完缓存,让其他线程请求缓存即可。

在这里我们采用第一种方式,让key永远不过期。

那可能有的人会说了,这很简单啊,那我就设置一个定时任务定时的去刷新key就可以了啊。于是写出了如下的定时作业代码。

// 定时任务,每隔30分钟,从数据库中读取商品列表,存储到缓存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {//从数据库中查询参加活动的商品列表List<Product> productList = productMapper.queryAcitvityProductList();//删除旧的redisTemplate.delete(PRODUCT_LIST_KEY);//存储新的redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}

但是,不知道大家有没发现,我们即使加了定时任务的代码也会发生缓存击穿的问题。因为删除旧的数据 和 存储新的数据两个命令非原子操作,存在时间间隔。如果改用string结构存储,可以直接覆盖旧值,则没有原子性问题,但是业务需求需要支持分页,只能用list结构。

	//就在我删除旧的key的时候,这会还没有往redis中放入,大的并发量进来导致请求都跑到了数据库上,造成缓存击穿。//删除旧的redisTemplate.delete(PRODUCT_LIST_KEY);//存储新的redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)

解决方案

  • 业务架构里面强调降级,兜底数据,那缓存击穿是不是也可以考虑这个方案,空间换时间

  • 缓存两份数据,一份是List结构(先删除,再设置新值), 一份是String结构(直接覆盖旧值)

    • 查询的时候优先查询list结构,如果没有则解析String结构成为list,进行内存分页,一般数据量不大
// 定时任务,每隔30分钟,从数据库中读取商品列表,存储到缓存里面
priviate static final String PRODUCT_LIST_KEY = "product:list";
priviate static final String PRODUCT_LIST_KEY_STR = "product:liststr";@Scheduled(cron = "0 */30 * * * ?")
public void loadActivityProduct() {//从数据库中查询参加活动的商品列表List<Product> productList = productMapper.queryAcitvityProductList();//先缓存一份String类型的数据,直接set,如果要分页则解析成list再返回redis.opsForValue.set(PRODUCT_LIST_KEY_STR, JSON.toString(productList))//删除旧的redisTemplate.delete(PRODUCT_LIST_KEY);//存储新的redis.opsForList.leftPushAll(PRODUCT_LIST_KEY, productList)
}

查询的时候,先去查list结构,list结构如果没有数据,则查String类型的数据。

priviate static final String PRODUCT_LIST_KEY = "product:list";
priviate static final String PRODUCT_LIST_KEY_STR = "product:liststr";// 将商品列表从 Redis 缓存中读取
public List<Product> getProductListFromCache(int begin, int end) {List<Product> list = new ArrayList();//从缓存里分页获取list = redisTemplate.opsForList().range(PRODUCT_LIST_KEY, begin,end)if (productListStr != null) {return list;} else {// 缓存A中不存在商品列表,则从缓存B读取String productStrList = redis.opsForValue.get(PRODUCT_LIST_KEY_STR);// 缓存中存在商品列表,将 JSON 字符串转换为对象List<Product> productList = JSON.parseArray(productStrList, Product.class);//分页计算list = CommonUtil.pageList(productList,begin, end);return list;}
}

OK,整篇的案例整合 我们就到这里,觉得博主写的不错的,记得给个三连哦!!!

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/1913.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis分布式问题

Redis实现分布式锁 Redis为单进程单线程模式&#xff0c;采用队列模式将并发访问变成串行访问&#xff0c;且多客户端对Redis的连接并不存在竞争关系Redis中可以使用SETNX命令实现分布式锁。当且仅当 key 不存在&#xff0c;将 key 的值设为 value。 若给定的 key 已经存在&…

『赠书活动 | 第十三期』《算力经济:从超级计算到云计算》

&#x1f497;wei_shuo的个人主页 &#x1f4ab;wei_shuo的学习社区 &#x1f310;Hello World &#xff01; 『赠书活动 &#xff5c; 第十三期』 本期书籍&#xff1a;《算力经济&#xff1a;从超级计算到云计算》 赠书规则&#xff1a;评论区&#xff1a;点赞&#xff5c;收…

全志V3S嵌入式驱动开发(开发环境再升级)

【 声明&#xff1a;版权所有&#xff0c;欢迎转载&#xff0c;请勿用于商业用途。 联系信箱&#xff1a;feixiaoxing 163.com】 前面我们陆陆续续开发了差不多有10个驱动&#xff0c;涉及到网口、串口、音频和视频等几个方面。但是整个开发的效率还是比较低的。每次开发调试的…

Matlab论文插图绘制模板第105期—带缺口的分组填充箱线图

在之前的文章中&#xff0c;分享了Matlab带缺口的分组箱线图的绘制模板&#xff1a; 进一步&#xff0c;再来分享一下带缺口的分组填充箱线图的绘制模板。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自…

筹码分布图高级用法——历史换手衰减系数自动计算公式

在使用筹码分布图时&#xff0c;很多人习惯于采用软件的默认设置&#xff0c;然而默认设置不一定能满足我们的要求。今天将向大家介绍筹码分布图的高级用法——历史换手衰减系数&#xff0c;并编写历史换手衰减系数自动计算公式。有些网友认为通过修改衰减系数&#xff0c;可以…

【C++实现二叉树的遍历】

目录 一、二叉树的结构二、二叉树的遍历方式三、源码 一、二叉树的结构 二、二叉树的遍历方式 先序遍历&#xff1a; 根–>左–>右中序遍历&#xff1a; 左–>根–>右后序遍历&#xff1a;左–>右–>根层次遍历&#xff1a;顶层–>底层 三、源码 注&am…

记事本软件误删后如何找回?

随着智能手机的普及&#xff0c;各种优秀的手机软件层出不穷&#xff0c;成为我们生活和工作中的得力助手。其中&#xff0c;记事本软件在手机上的应用也越来越受欢迎。 一款记事本可以给用户带来许多便利和帮助。与传统的纸质记事本相比&#xff0c;手机记事本具有更多的功能…

mac ppt设置起始页码

今天发现我的ppt的左边ppt的缩略图的开始页码是从2开始的&#xff0c;觉得很奇怪&#xff0c;这个解决的办法就是 点击ppt->文件->页面设置->页眉和页脚->幻灯片编号

SQL注入攻击与防护

目录 一、SQL注入攻击概述 1.1 SQL注入概念 1.1.1 标准查询过程 1.1.2 SQL注入定义 1.2 SQL注入根本原因 1.3 SQL注入条件 1.4 SQL注入防范 1.4.1 根本原因&#xff1a;过滤不严 1.4.2 安全设计原则&#xff1a;数据与代码分离 1.5 SQL注入流程 1.6 SQL注入分类 1.…

时序预测 | MATLAB实现PSO-LSTM(粒子群优化长短期记忆神经网络)时间序列预测

时序预测 | MATLAB实现PSO-LSTM(粒子群优化长短期记忆神经网络)时间序列预测 目录 时序预测 | MATLAB实现PSO-LSTM(粒子群优化长短期记忆神经网络)时间序列预测预测效果基本介绍模型介绍PSO模型LSTM模型PSO-LSTM模型 程序设计参考资料致谢 预测效果 基本介绍 Matlab基于PSO-LST…

Java日志框架介绍

​今天来聊一聊 Java 日志框架&#xff0c;不管是在项目开发阶段的调试&#xff0c;还是项目上线后的运行&#xff0c;都离不开日志。日志具有处理历史数据、定位程序问题、理解程序运行过程等重要作用。在 Spring 项目开发过程中我们常见的日志框架可能就是 logback、log4j2 和…

数据库的操作

前言 在之前的文章中&#xff0c;我们已经了解了什么是数据库&#xff0c;以及为什么有数据库&#xff0c;和数据库有什么作用&#xff0c;有了这些宏观概念之后&#xff0c;本章为大家进一步详细介绍对于数据库在Linux上如何具体操作。 1.创建数据库 1.1创建数据库语法 语法…