Spring Boot整合Redis实现分页查询

Spring Boot整合Redis实现分页查询

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条

pagepagekey是否超页value
question:0ques:2020-03-16超页0:4
question:1ques:2020-03-16超页5:9
question:2ques:2020-03-16未超页10:-1
ques:2020-03-15超页0:0
question:3ques:2020-03-15未超页1:-1

3)测试2:

ques:2020-03-16 18条,

ques:2020-03-15 2条

5条/页 共20条

pagekey是否超页value
question:0ques:2020-03-16超页0:4
question:1ques:2020-03-16超页5:9
question:2ques:2020-03-16超页10:14
question:3ques: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缓存】实现对缓存数据实现排序和分页功能

  • 6
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值