您的位置:首页 > 数据库 > Redis

Spring Boot整合Redis实现分页查询

2020-03-31 07:46 791 查看

Spring Boot整合Redis实现分页查询

文章目录

  • 2)QuesCacheService
  • Ⅳ、Controller层
  • 4、前端设计:
  • 5、测试用例
  • 6、效果演示:
  • 7、注意事项:
  • 8、待改进:
  • 9、参考文档:
  • 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、计数器count
    private 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、参考文档:

    1、Class与Style绑定

    2、vue v-for循环的用法

    3、Vue.js的核心思想

    4、【Redis缓存】实现对缓存数据实现排序和分页功能

    • 点赞
    • 收藏
    • 分享
    • 文章举报
    王小希ww 发布了13 篇原创文章 · 获赞 0 · 访问量 327 私信 关注
    内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
    标签: