目标:
心理医生可以到问题社区中,根据问题的提问日期,分页查询问题。
效果设计:
将mysql中热点问题(日期靠前的问题)保存在Redis缓存中。假设热点数据为20条,分页效果为5条/页,并设置redis的数据缓存时间。如果查询的数据不在前20条中,则需要到数据库中查找。(不知这样设计是否合理,实现后,感觉速度好像变慢了,也许redis设计的表太占内存了)
redis字段的设计:
第一次查询,直接在mysql中查出前20条记录。并用redis的list类型依次保存问题的日期(Date不重复),并用redis的set类型依次保存问题,key与Date的value对应。
前20条记录已经在redis缓存中,此时需要对这20条记录分页,并保存在缓存中。为了查找方便,用hash类型来保存,字段设计为:页号,日期,该日期的问题数:遍历下标。
3个表设计如下:
springboot + redis + thymeleaf + bootstrap + vue
表的设计与实现方式不唯一,如下代码仅是我的个人思考,仅供参考
将分页数据封装到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;
...
}
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();
...
}
备注:QuesService通过QuesCacheService,在内存中生成日期list,问题集合以及页面数据,并设置这些数据的过期时间
private static int count = 0; //记录缓存的数据
public Integer getQuesCount(){
return questionMapper.getQuesCount();
}
/**
* @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;
}
分页查询得到问题列表,并将前4页的问题按日期保存在redis中。(先同时生成list表和set表,再生成hash表)
/**
* @Description: 分页查询得到问题列表,并将问题按日期保存在redis中。
* 方便在redis中按日期查询。
* @Param: begin,end
* @return: List
* @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;
}
}
先将问题写入数据库。
若页面重新访问数据,此时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());
}
public void insertQuesDate(String date){
redisDateTemplate.opsForList().rightPush( "ques:date", date);
}
public void insertQuesByDate(String date,Question question){
redisQuesTemplate.opsForSet().add( date, question);
}
备注:最难处理的就是这,字段如何设计,才方便读取。这里的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;
}
}
//根据页号查出指定的问题集合
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;
}
//设置日期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);
}
@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;
}
}
返回得到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;
}
<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">
<div th:replace="/basic/pageLoader :: pageLoader">div>
<div class="overlay">div>
<div th:replace="/basic/topBar :: topBar">div>
<div th:replace="/basic/leftBar :: leftBar">div>
<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>
<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 class="body">
<ul class="pagination pagination-primary m-b-0">
<li v-if="prePage" class="page-item"><a class="page-link" @click="prePage">Previousa>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">Nexta>li>
ul>
div>
div>
div>
div>
section>
...
body>
html>
<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>
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 |
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
1)多线程访问时,如何保证mysql的service层的count(数据库中问题数目统计)数据一致性,以及redis的service层的count(缓存中问题数目的统计)的数据一致性
3)在redis缓存正在更新时,线程访问缓存,会出现数据读取错误,比如5条/页的数据也许会变成7条/页(测试)
1、Class与Style绑定
2、vue v-for循环的用法
3、Vue.js的核心思想
4、【Redis缓存】实现对缓存数据实现排序和分页功能