Spring Boot整合Redis实现分页查询
Spring Boot整合Redis实现分页查询
文章目录
- Spring Boot整合Redis实现分页查询
- 1、Demo描述:
- 2、框架使用:
- 3、后端设计:
- Ⅰ、Entity层
- Ⅱ、Dao层
- Ⅲ、Service层
- 1)QuesService
- a、计数器count
- b、查找问题总数
- c、得到pageData基本信息
- d、分页查询
- e、插入问题
1、Demo描述:
-
目标:
心理医生可以到问题社区中,根据问题的提问日期,分页查询问题。 -
效果设计:
将mysql中热点问题(日期靠前的问题)保存在Redis缓存中。假设热点数据为20条,分页效果为5条/页,并设置redis的数据缓存时间。如果查询的数据不在前20条中,则需要到数据库中查找。(不知这样设计是否合理,实现后,感觉速度好像变慢了,也许redis设计的表太占内存了)
-
redis字段的设计:
第一次查询,直接在mysql中查出前20条记录。并用redis的list类型依次保存问题的日期(Date不重复),并用redis的set类型依次保存问题,key与Date的value对应。
前20条记录已经在redis缓存中,此时需要对这20条记录分页,并保存在缓存中。为了查找方便,用hash类型来保存,字段设计为:页号,日期,该日期的问题数:遍历下标。
3个表设计如下:
2、框架使用:
springboot + redis + thymeleaf + bootstrap + vue
3、后端设计:
表的设计与实现方式不唯一,如下代码仅是我的个人思考,仅供参考
Ⅰ、Entity层
将分页数据封装到pageData中,并传给前端页面
package com.wangxiaoxi.mheal.entity; import org.thymeleaf.expression.Lists; import java.io.Serializable; import java.util.ArrayList; import java.util.List; /** * @author: wangxiaoxi * @create: 2020-03-08 17:00 **/ public class PageData<T> implements Serializable{ /** 数据集合 */ protected List<T> result = new ArrayList(); /** 数据总数 */ protected int totalCount = 0; /** 总页数 */ protected long pageCount = 0; /** 每页记录 */ protected int pageSize = 5; /** 初始当前页 */ protected int pageNo = 1; /**当前遍历问题集合的下标,默认到尾元素*/ protected int indexEnd = -1; ... }
Ⅱ、Dao层
mysql分页查询问题,并按日期递减排序
查询问题数量为了设置pageData的pageCount属性
@Mapper public interface QuestionMapper { ... @Results({ @Result(column = "id",property = "id"), @Result(column = "id",property = "student",one = @One(select = "com.wangxiaoxi.mheal.mapper.StudentMapper.getStuByQuesId",fetchType=FetchType.DEFAULT)), @Result(column = "id",property = "doctors",many = @Many(select = "com.wangxiaoxi.mheal.mapper.DoctorMapper.getDoctorsByQuesId",fetchType=FetchType.DEFAULT)), }) @Select("select * from question order by updateTime DESC limit #{arg0},#{arg1}") public List<Question> getQuestions(Integer begin, Integer end); @Select("select count(*) from question") public Integer getQuesCount(); ... }
Ⅲ、Service层
1)QuesService
备注:QuesService通过QuesCacheService,在内存中生成日期list,问题集合以及页面数据,并设置这些数据的过期时间
a、计数器countprivate static int count = 0; //记录缓存的数据b、查找问题总数
public Integer getQuesCount(){ return questionMapper.getQuesCount(); }c、得到pageData基本信息
/** * @Description: 得到pageData的基本信息 * @Param: * @return: * @Author: wangxiaoxi * @Date: 2020/3/13 0013 */ public PageData<Question> getPageData(PageData<Question> pageData) { //count每次需要到数据库中取数据,保证数据的时效性 count = questionMapper.getQuesCount(); pageData.setTotalCount(count); if(count % pageData.getPageSize() == 0){ pageData.setPageCount(count / pageData.getPageSize()); }else{ pageData.setPageCount(count / pageData.getPageSize() + 1); } if(pageData.getPageCount() == 0){ pageData.setPageCount(1); } return pageData; }d、分页查询
分页查询得到问题列表,并将前4页的问题按日期保存在redis中。(先同时生成list表和set表,再生成hash表)
/** * @Description: 分页查询得到问题列表,并将问题按日期保存在redis中。 * 方便在redis中按日期查询。 * @Param: begin,end * @return: List<Question> * @Author: wangxiaoxi * @Date: 2020/3/8 0008 */ public List<Question> getQuestions(PageData<Question> pageData, Integer begin, Integer end){ //若查找的是前4页数据,并且该数据在缓存中,则从缓存中取,并返回指定页的问题集合 if(!quesCacheService.isQuesEmpty() && begin <= 4 * pageData.getPageSize()){ System.out.println("redis"); int pageNum = pageData.getPageNo(); List<Question> questions = quesCacheService.getQuesByPage(pageNum - 1); return questions; } //先到数据库中,按日期先取4页数据 else if(begin <= 4 * pageData.getPageSize()){ System.out.println("sql"); List<Question> questions = questionMapper.getQuestions(0, 4 * pageData.getPageSize()); String time; for (Question question: questions) { time = question.getUpdateTime().split(" ")[0]; time = "ques:" + time; //将问题按日期插入到redis,数据类型为set,并设置过期时间 quesCacheService.insertQuesByDate(time,question); //将日期插入到redis,数据类型为list,并设置过期时间 if(!quesCacheService.isQuesDateEmpty(time)){ quesCacheService.insertQuesDate(time); quesCacheService.setQuesExpire(time,1); } } quesCacheService.setDateExpire("ques:date",1); //根据问题集合生成分页表,数据类型是hash,并设置过期时间 List<String> dates = quesCacheService.getQuesDate(); for (String date: dates) { Set<Question> quesSet = quesCacheService.getQuesByDate(date); pageData = quesCacheService.insertQuesPage(pageData,date,quesSet); //若pageData的indexEnd不是-1,则该集合还有元素未遍历,需要重新再来 while(pageData.getIndexEnd() != -1){ pageData = quesCacheService.insertQuesPage(pageData,date,quesSet); } } //hash表生成成功,将缓存数据个数count置0 QuesCacheService.setCount(0); //设置页面过期时间 setPagesExpire(pageData); return quesCacheService.getQuesByPage(pageData.getPageNo()); } else{ System.out.println("sql"); List<Question> questions = questionMapper.getQuestions(begin, end); return questions; } }e、插入问题
先将问题写入数据库。
若页面重新访问数据,此时redis缓存中数据未过期,则访问原来的数据。
若redis中数据过期,则重新访问mysql,并将新的数据加载到redis缓存中。
@Transactional public void insertQues(Question question,Student student){ question.setId(UUID.randomUUID().toString()); System.out.println(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(System.currentTimeMillis()))); question.setCreateTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(System.currentTimeMillis()))); question.setUpdateTime(new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date(System.currentTimeMillis()))); question.setViewCount(0); question.setLikes(0); //插入问题 questionMapper.insertQues(question); //插入中间表 questionMapper.insertQuesWithStu(question.getId(),student.getId()); }
2)QuesCacheService
a、生成date表
public void insertQuesDate(String date){ redisDateTemplate.opsForList().rightPush( "ques:date", date); }b、生成set表
public void insertQuesByDate(String date,Question question){ redisQuesTemplate.opsForSet().add( date, question); }c、生成hash表
备注:最难处理的就是这,字段如何设计,才方便读取。这里的key为question:日期,value为begin:end(左闭右闭)
假设
ques:2020-3-8 3条
ques:2020-3-7 3条
ques:2020-3-6 1条
ques:2020-3-5 2条
ques:2020-3-4 4条
ques:2020-3-3 2条
ques:2020-3-2 14条
ques:2020-3-1 5条
处理逻辑如下:
/** * 将分页数据,用redis的hash保存 * 1)判断该问题是否之前遍历过,并计算size * 2)如果未超页,则count:indexEnd 为 size:-1。(注意size为本来问题的数目,不是遍历后剩余的size) * 3)如果超页,则count:indexEnd 为 size:前一个indexEnd + pageSize (注意size为本来问题的数目,不是遍历后剩余的size) * 并将下一个页号保存在pageData中,将其返回 * @param pageData * @param date * @param questions * @return */ public PageData<Question> insertQuesPage(PageData<Question>pageData, String date, Set<Question> questions){ int leftOverSize; int indexBegin = pageData.getIndexEnd(); int indexEnd; //indexBegin == -1, 上一个集合遍历完毕,leftOverSize为新的问题的size, // 此时需要判断leftOverSize + count是否超页,不超页,直接加;若超页,还需要修改leftOverSize if(indexBegin == -1) { leftOverSize = questions.size(); } //上一个集合还有元素未遍历完,leftOverSize是旧问题的size,由于之前遍历过,size需要减去之前的元素个数才能变成leftOverSize else{ leftOverSize = questions.size() - indexBegin - 1; } int temp = (pageData.getPageSize() * pageData.getPageNo()); //加上该日期下的剩下的所有问题,未超页 if(count + leftOverSize <= temp){ count = count + leftOverSize; //如果上一次遍历完(indexBegin == -1),则hv为 0 :-1, if(indexBegin == -1){ redisQuesTemplate.opsForHash().put("question:" + (pageData.getPageNo() - 1), date, 0 + ":-1"); } // 如果上一次未遍历完indexBegin != -1,则hv还是 (indexBegin + 1): -1 else{ redisQuesTemplate.opsForHash().put("question:" + (pageData.getPageNo() - 1), date, (indexBegin + 1) + ":-1"); } pageData.setPageNo(pageData.getPageNo()); pageData.setIndexEnd(-1); //如果刚好满页,则将pageNo设置为下一页 if(count == temp){ pageData.setPageNo(pageData.getPageNo() + 1); } return pageData; } /** * 超页,hv为indexBegin: indexEnd, 新的indexEnd = indexBegin + min(pageSize ,leftOverSize) * 需要返回新的页号,和该问题集合的问题下标,方便下次重新加载问题集合 */ else{![Hash字段](E:\java面试\Study_Repositories\images\springboot\Hash字段.png) //上一个问题遍历完毕,且这个问题若全部加入会超页,需要修改leftOverSize if(indexBegin == -1){ indexBegin = 0; leftOverSize = pageData.getPageSize() * pageData.getPageNo() - count; }else{ indexBegin = indexBegin + 1; } indexEnd = indexBegin + Math.min(pageData.getPageSize() - 1,leftOverSize - 1); redisQuesTemplate.opsForHash().put("question:" + (pageData.getPageNo() - 1),date,indexBegin + ":" + indexEnd); pageData.setPageNo(pageData.getPageNo() + 1); pageData.setIndexEnd(indexEnd); count = temp; return pageData; } }d、根据页号查出指定的问题集合
//根据页号查出指定的问题集合 public List<Question> getQuesByPage(int pageNum) { List<Question> questions = new ArrayList<>(); //从redis中得到日期list Set dates = redisQuesTemplate.opsForHash().keys("question:" + pageNum); for(Object date : dates) { String countAndIndex = (String) redisQuesTemplate.opsForHash().get("question:" + pageNum, date); Integer indexBegin = Integer.valueOf(countAndIndex.split(":")[0]); Integer indexEnd = Integer.valueOf(countAndIndex.split(":")[1]); //通过日期获取问题set Set<Question> questionSet = getQuesByDate((String) date); int index = 0; //从indexBegin开始,indexEnd结束读取问题 for (Question q : questionSet) { if (indexEnd != -1) { if (indexBegin <= index && index <= indexEnd) { questions.add(q); } else if (index > indexEnd) { break; } } else { //indexEnd == -1 加至最后 if (indexBegin <= index) { questions.add(q); } } index++; } } return questions; }e、设置过期时间
//设置日期list过期时间 public void setDateExpire(String date,Integer minutes){ redisDateTemplate.expire(date,minutes, TimeUnit.MINUTES); } //设置问题set的过期时间 public void setQuesExpire(String date,Integer minutes){ redisQuesTemplate.expire(date,minutes, TimeUnit.MINUTES); } //设置页面hash的过期时间 public void setPageExpire(String page,Integer minutes){ redisQuesTemplate.expire(page,minutes, TimeUnit.MINUTES); }f、其余的方法
@Service public class QuesCacheService { @Autowired private RedisTemplate<String,Question> redisQuesTemplate; //问题按日期划分 @Autowired private StringRedisTemplate redisDateTemplate; //加载进redis缓存的日期列表 private static int count = 0; //记录缓存的数据 //日期list是否为空 public boolean isQuesDateEmpty(String date){ List<String> list = getQuesDate(); return list.contains(date); } //得到日期list public List<String> getQuesDate(){ return redisDateTemplate.opsForList().range("ques:date",0,-1); } //得到问题set public Set<Question> getQuesByDate(String date){ return redisQuesTemplate.opsForSet().members(date); } //日期list是否为空 public boolean isQuesEmpty(){ if(redisDateTemplate.opsForList().range("ques:date",0,-1).size() == 0){ return true; } return false; } }g、三个表生成成功
Ⅳ、Controller层
返回得到json数据,方便前台的axios异步调用
/** * @Description: 返回pageData的json数据 * @Param: * @return: * @Author: wangxiaoxi * @Date: 2020/3/12 0012 */ @GetMapping(value = "/question/pageData") @ResponseBody public PageData<Question> getPageData(PageData<Question> pageData,HttpServletRequest servletRequest){ System.out.println("getPageData"); //设置每页数据条数 pageData.setPageSize(5); pageData = quesService.getPageData(pageData); System.out.println(pageData); //注意limit语法:select * from table limit (start-1)*pageSize,pageSize int begin = (pageData.getPageNo() - 1) * pageData.getPageSize(); int end = pageData.getPageSize(); List<Question> questions = quesService.getQuestions(pageData,begin,end); pageData.setResult(questions); System.out.println(pageData); return pageData; }
4、前端设计:
Ⅰ、questionHood.html
<!doctype html> <html xmlns:th="http://www.thymeleaf.org" xmlns:v-bind="http://www.w3.org/1999/xhtml" class="no-js " lang="en"> <head> ... </head> <body class="theme-blue ls-toggle-menu"> <!-- Page Loader --> <div th:replace="/basic/pageLoader :: pageLoader"></div> <!-- Overlay For Sidebars --> <div class="overlay"></div> <!-- Top Bar --> <div th:replace="/basic/topBar :: topBar"></div> <!-- Left Sidebar --> <div th:replace="/basic/leftBar :: leftBar"></div> <!-- Right Sidebar --> <div th:replace="/basic/rightBar :: rightBar"></div> <section class="content inbox"> <div class="block-header"> <div class="row"> <div class="col-lg-7 col-md-6 col-sm-12"> <h2>在线问答</h2> </div> <div class="col-lg-5 col-md-6 col-sm-12"> </div> </div> </div> <div class="card"> <div id="app" class="container-fluid"> <div class="header row clearfix"> <h2><strong>日期</strong></h2> <!-- row1 --> <div id="answer" class="body col-lg-12 col-md-12 col-sm-12"> <!-- 问 --> <ul class="mail_list list-group list-unstyled"> <li class="list-group-item" v-for="question in questions"> <div class="media"> <div class="pull-left"> <small style="color: #0d97ff">{{question.updateTime}}</small> <div class="thumb hidden-sm-down m-r-20"><img th:src="@{/assets/images/xs/avatar1.jpg}" class="rounded-circle" alt=""> </div> </div> <div class="media-body"> <div class="media-heading"> <a href="mail-single.html" class="m-r-10">小红</a> <span class="badge bg-blue">压力</span> <a href="mail-compose.html" style="color:#3eacff;" class="pull-right"><small class="float-right">点击回答</small></a> </div> <p class="msg">{{question.content}}</p> <hr> </div> </div> </li> </ul> </div> </div> <div class="card m-t-5"> <!--报错--> <!--<div id="app1" class="body">--> <div class="body"> <ul class="pagination pagination-primary m-b-0"> <li v-if="prePage" class="page-item"><a class="page-link" @click="prePage">Previous</a></li> <!--注意三元表达式和数组对象语法的区别--> <li v-bind:class="[{active:isActive == count},pageItem]" v-for="count in pageCount"> <a class="page-link" @click="pageSelect(count)" v-text="count"></a> </li> <li v-if="nextPage" class="page-item"><a class="page-link" @click="nextPage">Next</a></li> </ul> </div> </div> </div> </div> </section> <!-- Jquery Core Js --> ... </body> </html>
Ⅱ、vue
<script th:inline="javascript"> var app = new Vue({ //注意el只能对一个顶层元素以及其后代元素有效 el:"#app", data: { pageCount:{}, questions:[], isActive:1, pageItem: 'page-item', prePage: false, nextPage: false }, methods:{ pageSelect : async function (pageNo) { //如果页面数为1,不显示next,pre if(this.pageCount == 1){ this.nextPage= false; this.prePage = false; } //如果页面数不为1,分情况讨论next,pre显示情况 else{ if(pageNo == this.pageCount){ this.prePage = true; this.nextPage= false; } else if(pageNo == 1){ this.prePage = false; this.nextPage= true; }else{ this.prePage = true; this.nextPage= true; } } // alert(pageNo) _this = this; try{ await axios.get("/mheal/question/pageData?pageNo=" + pageNo) //lambda表达式如何写 .then(res => { _this.questions = res.data.result; }) }catch (err){ console.log(err) } this.isActive = pageNo; }, //下一页 nextPage : async function(){ if(this.isActive + 1 <= this.pageCount){ if(this.isActive + 1 == this.pageCount){ this.nextPage = false; } this.isActive = this.isActive + 1; _this = this; try{ await axios.get("/mheal/question/pageData?pageNo=" + this.isActive ) //lambda表达式如何写 .then(res => { _this.questions = res.data.result; }) }catch (err){ console.log(err) } } }, // 上一页 prePage: async function(){ if(this.isActive - 1 >= 0){ if(this.isActive - 1 == 0){ this.prePage = false; } this.isActive = this.isActive - 1; _this = this; try{ await axios.get("/mheal/question/pageData?pageNo=" + this.isActive ) //lambda表达式如何写 .then(res => { _this.questions = res.data.result; }) }catch (err){ console.log(err) } } } }, //created同步方法如何写 created: async function(){ _this = this; try{ await axios.get("/mheal/question/pageData?pageNo=1") //lambda表达式如何写 .then(res => { _this.pageCount = res.data.pageCount; _this.questions = res.data.result }) }catch (err){ console.log(err) } console.log(this.questions) this.prePage = false; if(this.pageCount == 1){ this.nextPage = false; }else{ this.nextPage = true; } } }) </script>
5、测试用例
1)测试用例主要是用来测试页号和问题映射的hash表写入和读取是否正确,问题集合可以分为:将剩余问题加入超过页面,将剩余问题加入未超过页面,将剩余问题加入刚好满页。
2)测试1:
ques:2020-03-16 14条,
ques:2020-03-15 2条
5条/页 共16条
pagepage | key | 是否超页 | value |
---|---|---|---|
question:0 | ques:2020-03-16 | 超页 | 0:4 |
question:1 | ques:2020-03-16 | 超页 | 5:9 |
question:2 | ques:2020-03-16 | 未超页 | 10:-1 |
ques:2020-03-15 | 超页 | 0:0 | |
question:3 | ques:2020-03-15 | 未超页 | 1:-1 |
3)测试2:
ques:2020-03-16 18条,
ques:2020-03-15 2条
5条/页 共20条
page | key | 是否超页 | value |
---|---|---|---|
question:0 | ques:2020-03-16 | 超页 | 0:4 |
question:1 | ques:2020-03-16 | 超页 | 5:9 |
question:2 | ques:2020-03-16 | 超页 | 10:14 |
question:3 | ques:2020-03-16 | 未超页 | 14:-1 |
ques:2020-03-15 | 满页 | 0:-1 |
6、效果演示:
7、注意事项:
1)vue的created方法异步处理:created: async function(){}
2)vue的el挂载:el只能对一个顶层元素以及其后代元素有效
3)v-for如何迭代元素和数字: v-for=“count in pageCount”
4)v-bind 三元表达式和简洁写法如何写:v-bind:class="[{active:isActive == count},pageItem]"
5)lambda表达式如何写: (res => { _this.questions = res.data.result; })
6)vue核心思想:数据绑定,dom对象的操作,vue在底层已经实现了,组件化
7)注意mysql分页查询中limit的语法,*select * from table limit (start-1)pageSize, pageSize
8、待改进:
1)多线程访问时,如何保证mysql的service层的count(数据库中问题数目统计)数据一致性,以及redis的service层的count(缓存中问题数目的统计)的数据一致性
3)在redis缓存正在更新时,线程访问缓存,会出现数据读取错误,比如5条/页的数据也许会变成7条/页(测试)
9、参考文档:
- 点赞
- 收藏
- 分享
- 文章举报
- [分页查询]SpringBoot整合PageHelper分页插件实现简单的分页查询(含前端及后端代码)
- spring boot整合reids 然后实现缓存分页(方法之一) 以及RedisTemplate存到reids 里面get 就消失的坑
- 初学spring boot 记录下过程-整合mybatis实现分页查询(四)
- springboot整合pagehelper实现分页查询
- Spring Boot 整合 Elasticsearch,实现 function score query 权重分查询
- 在Spring Boot中使用Spring-data-jpa实现分页查询
- 在Spring Boot中使用Spring-data-jpa实现分页查询(转)
- SpringBoot 整合redis实现缓存 记录@CachePut值为1
- Spring Boot 整合 Redis 实现缓存操作
- springboot 简单查询 整合redis
- springboot整合mybatis进行分页查询
- SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例
- Spring Boot 整合 Elasticsearch,实现 function score query 权重分查询
- SpringBoot整合Spring-data-redis实现集中式缓存
- springboot 查询 整合redis
- SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例
- Springboot整合JPA以及动态条件查询的实现
- spring boot 整合shiro和redis实现权限管理和登录功能
- 简单的秒杀商品实现,SpringBoot整合redis和RabbitMQ学习笔记
- SpringBoot-07:SpringBoot整合PageHelper做多条件分页查询