第四天
高并发优化
前端每隔15秒就发起一次请求,将播放记录写入数据库。
但问题是,提交播放记录的业务太复杂了,其中涉及到大量的数据库操作:在并发较高的情况下,会给数据库带来非常大的压力
使用Redis合并写请求
一方面我们要缓存写数据,减少写数据库频率;另一方面我们要缓存播放记录,减少查询数据库。因此,缓存中至少要包含3个字段:
- 记录id:id,用于根据id更新数据库
- 播放进度:moment,用于缓存播放进度
- 播放状态(是否学完):finished,用于判断是否是第一次学完
可以使用redis种Hash结构
DelayQueue | Redisson | MQ | 时间轮 | |
原理 | JDK自带延迟队列,基于阻塞队列实现。 | 基于Redis数据结构模拟JDK的DelayQueue实现 | 利用MQ的特性。例如RabbitMQ的死信队列 | 时间轮算法 |
优点 |
|
|
|
|
缺点 |
|
|
|
|
优化代码
改的时绿框的2
把原来视频处理的第一步查询数据库的旧数据改为先查询缓存,缓存没有在查询数据库并返回缓存(这里封装成一个方法了方便业务逻辑)
/**
* 查询旧的学习记录
*
* @param userId
* @param dto
*/
private LearningRecord queryOldRecord(Long userId, LearningRecordFormDTO dto) {
//1。查询缓存
LearningRecord learningRecord = recordDelayTaskHandler.readRecordCache(dto.getLessonId(), dto.getSectionId());
//2.查到数据直接方法
if (learningRecord != null) {
return learningRecord;
}
//3.没有查询到就数据库
LearningRecord dbrecord = this.lambdaQuery()
.eq(LearningRecord::getUserId, userId)
.eq(LearningRecord::getLessonId, dto.getLessonId())
.eq(LearningRecord::getSectionId, dto.getSectionId())
.one();
//4.把数据库查询到的数据放入缓存
if(dbrecord == null){
return null;
}
recordDelayTaskHandler.writeRecordCache(dbrecord);
return dbrecord;
}
这里时绿框的1和3
//3.不是第一次学习--更新学习记录--moment//finished和finishTime取决与是否第一次学完
//3.1判断是否第一次学完
Boolean finished = record.getFinished();//是否学完
Integer moment = record.getMoment();//视频的当前观看时长,单位秒
Integer duration = dto.getDuration();//视频总时长
boolean isFinished = !finished && moment * 2 > duration;//是否第一次学完
if(!isFinished){
//不是第一次学完
//学习记录到缓存
LearningRecord learningRecord = new LearningRecord();
learningRecord.setLessonId(dto.getLessonId());
learningRecord.setSectionId(dto.getSectionId());
learningRecord.setMoment(dto.getMoment());
learningRecord.setFinished(finished);
learningRecord.setId(record.getId());
//提交延迟任务
recordDelayTaskHandler.addLearningRecordTask(learningRecord);
return finished;
}
//第一次学完,更新数据库
boolean update = this.lambdaUpdate()
.set(LearningRecord::getMoment, dto.getMoment())
.set(isFinished, LearningRecord::getFinished, isFinished)
.set(isFinished, LearningRecord::getFinishTime, dto.getCommitTime())
.eq(LearningRecord::getUserId, userId)
.eq(LearningRecord::getLessonId, dto.getLessonId())
.eq(LearningRecord::getSectionId, dto.getSectionId())
.update();
if (!update) {
throw new DbException("更新学习记录失败");
}
//还需要清理缓存
recordDelayTaskHandler.cleanRecordCache(dto.getLessonId(), dto.getSectionId());
return true;
第五天
1.新增和修改互动问题
简单的crud---无
2.用户端分页查询问题列表
⭐Mybatis-plus中排除某个字段
1.直接把需要的字段用.select(InteractionQuestion::getId.......)
Page<InteractionQuestion> page = this.lambdaQuery()
//不需要返回问题描述
//.select(InteractionQuestion::getId, InteractionQuestion::getTitle, InteractionQuestion::getCourseId)
//.select(InteractionQuestion.class, info -> !info.getProperty().equals("description"))
.select(InteractionQuestion.class, new Predicate<TableFieldInfo>() {
@Override
public boolean test(TableFieldInfo tableFieldInfo) {
//指定不用查询的字段
// tableFieldInfo.getProperty();获取InteractionQuestion中的属性名称
return !tableFieldInfo.getProperty().equals("description");
}
})
.eq(courseId != null, InteractionQuestion::getCourseId, courseId)
.eq(sectionId != null, InteractionQuestion::getSectionId, sectionId)
.eq(query.getOnlyMine(), InteractionQuestion::getUserId, userId)
.eq(InteractionQuestion::getHidden, false)
.page(query.toMpPageDefaultSortByCreateTimeDesc());
⭐Steam流--过滤
Set<Long> userIds = new HashSet<>();
Set<Long> answerIds = new HashSet<>();
for (InteractionQuestion record : records) {
//获取没匿名的提问者id集合
if(!record.getAnonymity()){
userIds.add(record.getUserId());
}
//最新回答的id集合
if(record.getLatestAnswerId()!=null) {
answerIds.add(record.getLatestAnswerId());
}
}
Set<Long> answerIds = records.stream()
.filter(c -> c.getLatestAnswerId() != null)
.map(InteractionQuestion::getLatestAnswerId)
.collect(Collectors.toSet());
Set<Long> userIds = records.stream()
.filter(interactionQuestion -> !interactionQuestion.getAnonymity())
.map(InteractionQuestion::getUserId)
.collect(Collectors.toSet());
完整代码
/**
* 用户端分页查询问题列表
*
* @param query
* @return
*/
@Override
public PageDTO<QuestionVO> pageQuestion(QuestionPageQuery query) {
//如果用户是匿名提问,则不应返回提问者信息
//如果是被管理端隐藏的问题,不应返回
Long userId = UserContext.getUser();
//1.校验参数
Long courseId = query.getCourseId();
Long sectionId = query.getSectionId();
if (courseId == null && sectionId == null) {
throw new BadRequestException("参数错误");
}
//2.分页查询--interaction_question 条件:
// course_id
// onMine为ture才加
// userId
// 小节id不为空才加
// Hidden为false(不被隐藏)
// 分页id
// 按照提问时间排序
Page<InteractionQuestion> page = this.lambdaQuery()
//不需要返回问题描述
//.select(InteractionQuestion::getId, InteractionQuestion::getTitle, InteractionQuestion::getCourseId)
//.select(InteractionQuestion.class, info -> !info.getProperty().equals("description"))
.select(InteractionQuestion.class, new Predicate<TableFieldInfo>() {
@Override
public boolean test(TableFieldInfo tableFieldInfo) {
//指定不用查询的字段
// tableFieldInfo.getProperty();获取InteractionQuestion中的属性名称
return !tableFieldInfo.getProperty().equals("description");
}
})
.eq(courseId != null, InteractionQuestion::getCourseId, courseId)
.eq(sectionId != null, InteractionQuestion::getSectionId, sectionId)
.eq(query.getOnlyMine(), InteractionQuestion::getUserId, userId)
.eq(InteractionQuestion::getHidden, false)
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List<InteractionQuestion> records = page.getRecords();
if (CollUtils.isEmpty(records)) {
return PageDTO.empty(page);
}
//查询最新回答者id和最新问题的回答id
Set<Long> userIds = new HashSet<>();
Set<Long> answerIds = new HashSet<>();
for (InteractionQuestion record : records) {
//获取没匿名的提问者id集合
if (!record.getAnonymity()) {
userIds.add(record.getUserId());
}
//最新回答的id集合
if (record.getLatestAnswerId() != null) {
answerIds.add(record.getLatestAnswerId());
}
}
// Set<Long> answerIds = records.stream()
// .filter(c -> c.getLatestAnswerId() != null)
// .map(InteractionQuestion::getLatestAnswerId)
// .collect(Collectors.toSet());
//
// Set<Long> userIds = records.stream()
// .filter(interactionQuestion -> !interactionQuestion.getAnonymity())
// .map(InteractionQuestion::getUserId)
// .collect(Collectors.toSet());
//3.查询最近一次的回答---interaction_reply 条件:latest.answer_id
Map<Long, InteractionReply> replyMap = new HashMap<>();
if (!CollUtils.isEmpty(answerIds)) {
List<InteractionReply> latestAnswers = replyService.lambdaQuery()
.in(InteractionReply::getId, answerIds)
.list();
//转成map
replyMap = latestAnswers.stream()
.collect(Collectors.toMap(InteractionReply::getId, r -> r));
//查到的是user_id--要把user_id转成user_name(直接放入userIds)
//latestAnswers.forEach(r -> userIds.add(r.getUserId()));
for (InteractionReply latestAnswer : latestAnswers) {
if (!latestAnswer.getAnonymity()) {
userIds.add(latestAnswer.getTargetUserId());
}
}
}
//4.查询最近回答的用户--远程调用用户服务
Map<Long, UserDTO> userDTOMap = new HashMap<>();
if (CollUtils.isEmpty(userIds)) {
List<UserDTO> userDTOS = userClient.queryUserByIds(userIds);
//list转map
userDTOMap = userDTOS.stream()
.collect(Collectors.toMap(UserDTO::getId, u -> u));
}
//5.封装DTO返回
List<QuestionVO> voList = new ArrayList<>();
for (InteractionQuestion record : records) {
QuestionVO vo = BeanUtils.copyBean(record, QuestionVO.class);
if (!vo.getAnonymity()) {
UserDTO userDTO = userDTOMap.get(record.getUserId());
//最新回答者信息
vo.setUserName(userDTO.getName());//昵称
vo.setUserIcon(userDTO.getIcon());//头像
}
//最新回答信息
InteractionReply reply = replyMap.get(record.getLatestAnswerId());
if (reply != null) {
vo.setLatestReplyContent(reply.getContent());//最新回答内容
if (!reply.getAnonymity()) {//最新回答者不是匿名的
vo.setLatestReplyUser(userDTOMap.get(reply.getUserId()).getName());//最新回答者昵称
}
}
voList.add(vo);
}
return PageDTO.of(page, voList);
}
3.用户端根据id查询问题详情
⭐️️4.管理端分页查询问题列表
1.如果前端传了课程名称,先es中得到课程id(注意这里是es中的课程id)
//1.如果前端传了课程名称,先es中得到es中课程id
boolean haveCourseIdList = false;
List<Long> coursesIdList = new ArrayList<>();
if (StringUtils.isNotBlank(query.getCourseName())) {
coursesIdList = searchClient.queryCoursesIdByName(query.getCourseName());
haveCourseIdList = CollUtils.isEmpty(coursesIdList);
if(haveCourseIdList){
return PageDTO.empty(0L, 0L);
}
2.分页查询---没有章节名称,小节名称,课程名称,分类名称,提问者名称
//2.分页查询 条件:前端条件+分页条件
Page<InteractionQuestion> page = this.lambdaQuery()
.in(haveCourseIdList, InteractionQuestion::getCourseId, coursesIdList)
.between(query.getBeginTime() != null && query.getEndTime() != null, InteractionQuestion::getCreateTime, query.getBeginTime(), query.getEndTime())
.eq(query.getStatus() != null, InteractionQuestion::getStatus, query.getStatus())
.page(query.toMpPageDefaultSortByCreateTimeDesc());
List<InteractionQuestion> records = page.getRecords();
if (CollUtils.isEmpty(records)) {
return PageDTO.empty(page);
}
3.先把章节id和小节id,课程id,提问者id都封装成集合
//提问学员id集合
Set<Long> userIdList = records.stream()
.map(InteractionQuestion::getUserId).collect(Collectors.toSet());
//章节和小节id集合
Set<Long> chapterAndSectionIdList = new HashSet<>();
chapterAndSectionIdList = records.stream()
.map(InteractionQuestion::getChapterId)
.collect(Collectors.toSet());
Set<Long> sectionIds = records.stream()
.map(InteractionQuestion::getSectionId)
.collect(Collectors.toSet());
chapterAndSectionIdList.addAll(sectionIds);
//表里面课程id集合
Set<Long> courseIdList = records.stream()
.map(InteractionQuestion::getCourseId).collect(Collectors.toSet());
4.根据课程id远程调用课程服务查询课程信息-------课程名称
//4.根据课程id远程调用课程服务查询课程信息
List<CourseSimpleInfoDTO> cInfoList = courseClient.getSimpleInfoList(coursesIdList);
if(CollUtils.isEmpty(cInfoList)){
throw new BadRequestException("课程不存在");
}
//转成map方便查询
Map<Long, CourseSimpleInfoDTO> cInfoMap = cInfoList.stream()
. collect(Collectors.toMap(CourseSimpleInfoDTO::getId, c -> c));
5.根据提问学员id远程调用查询用户表--------提问者名称
//5.根据提问学员id远程调用查询用户表
List<UserDTO> userDTOS = userClient.queryUserByIds(userIdList);
if(CollUtils.isEmpty(userDTOS)){
throw new BadRequestException("用户不存在");
}
//转成map方便查询
Map<Long, UserDTO> userDTOMap = userDTOS.stream()
.collect(Collectors.toMap(UserDTO::getId, u -> u));
6.根据章节id和小节id远程调用查询章节信息-----
//6.根据章节id和小节id远程调用查询分类信息
List<CataSimpleInfoDTO> infoDTOS = categoryClient.batchQueryCatalogue(chapterAndSectionIdList);
if(CollUtils.isEmpty(infoDTOS)){
throw new BadRequestException("章节或小节信息不存在");
}
//转成map方便查询
Map<Long, CataSimpleInfoDTO> infoMap = infoDTOS.stream()
.collect(Collectors.toMap(CataSimpleInfoDTO::getId, c -> c));
7.封装方法
List<QuestionAdminVO> voList = new ArrayList<>();
for (InteractionQuestion record : records) {
QuestionAdminVO vo = BeanUtils.copyBean(record, QuestionAdminVO.class);
UserDTO userDTO = uInfoMap.get(record.getUserId());
if(userDTO != null){
vo.setUserName(userDTO.getName());//提问者昵称
}
CourseSimpleInfoDTO cInfoDTO = cInfoMap.get(record.getCourseId());
if(cInfoDTO != null) {
vo.setCourseName(cInfoDTO.getName());//课程名称
//Caffeine
List<Long> categoryIds = cInfoDTO.getCategoryIds();
String categoryNames = categoryCache.getCategoryNames(categoryIds);
vo.setCategoryName(categoryNames);//三级分类名称,中间使用/隔开
}
CataSimpleInfoDTO CASInfoDTO = CASinfoMap.get(record.getChapterId());
if(CASInfoDTO != null) {
vo.setChapterName(CASInfoDTO.getName());//章名称
}
CataSimpleInfoDTO CASInfoDTO2 = CASinfoMap.get(record.getSectionId());
if(CASInfoDTO2 != null){
vo.setSectionName(CASInfoDTO2.getName());
}
// private String userName;
// private String courseName;
// private String chapterName;
// private String sectionName;
// private String categoryName;
}
return PageDTO.of(page, voList);
}
5.新增评论/回答
可以先把问题表的属性赋值了,最后一块修改数据库,不要多次查询修改
/**
* 新增评论/回答
*
* @param dto
*/
@Override
public void saveReply(ReplyDTO dto) {
//1.获取当前用户id
Long userId = UserContext.getUser();
//2.dto转成实体类
InteractionReply reply = BeanUtils.copyBean(dto, InteractionReply.class);
//3.新增评论/回答到回复表
boolean save = this.save(reply);
//4.如果是回答,更新问题表
InteractionQuestion question = new InteractionQuestion();
if (dto.getAnswerId() != null) {
//4.1.不是是回答是累加评论数
InteractionReply answerInfo = this.getById(dto.getAnswerId());
answerInfo.setReplyTimes(answerInfo.getReplyTimes() + 1);
this.updateById(answerInfo);
} else {
//4.2.是回答,累加回答次数
question = questionMapper.selectById(dto.getQuestionId());
question.setId(dto.getQuestionId());
question.setAnswerTimes(question.getAnswerTimes() + 1);
}
//5.是否是学生提交
if (dto.getIsStudent()) {
//6.是学生提交把问题表status字段设置为已查看
question.setStatus(QuestionStatus.CHECKED.getValue());
}
//等属性全部修改完,在更新数据库
questionMapper.updateById(question);
}
6.分页查询回答/评论
和上面的分页一模一样