设计一个企业网站大概多少钱,食品包装设计ppt,天津网站建设q479185700惠,动漫网站怎么建设学习记录-使用Redis合并写请求来优化性能
1.业务背景
学习进度的统计功能:为了更精确的记录用户上一次播放的进度#xff0c;采用的方案是#xff1a;前端每隔15秒就发起一次请求#xff0c;将播放记录写入数据库。但问题是#xff0c;提交播放记录的业务太复杂了#x…学习记录-使用Redis合并写请求来优化性能
1.业务背景
学习进度的统计功能:为了更精确的记录用户上一次播放的进度采用的方案是前端每隔15秒就发起一次请求将播放记录写入数据库。但问题是提交播放记录的业务太复杂了其中涉及到大量的数据库操作 2.解决方案思路
如图
由于数据都缓存到Redis了积累一些数据后再批量写入数据库这样数据库的写频率、写次数都大大减少对数据库压力小了非常多
优点
写缓存速度快响应时间大大减少降低数据库的写频率和写次数大大减轻数据库压力
缺点
实现相对复杂依赖Redis可靠性不支持事务和复杂业务
场景
写频率较高、写业务相对简单的场景
3.持久化思路
对于合并写请求方案一定有一个步骤就是持久化缓存数据到数据库。一般采用的是定时任务持久化
但是定时任务的持久化方式在播放进度记录业务中存在一些问题主要就是时效性问题。
假如定时任务间隔较短例如20秒一次对数据库的更新频率太高压力太大假如定时任务间隔较长例如2分钟一次更新频率较低时效性可能超过2分钟不满足需求
在学习记录统计场景下有什么办法能够在不增加数据库压力的情况下保证时间误差较低吗
假如一个视频时长为20分钟我们从头播放至15分钟关闭每隔15秒提交一次播放进度大概需要提交60次请求。
但是下一次我们再次打开该视频续播的时候肯定是从最后一次提交的播放进度来续播。也就是说续播进度之前的N次播放进度都是没有意义的都会被覆盖。既然如此完全没有必要定期把这些播放进度写到数据库只需要将用户最后一次提交的播放进度写入数据库即可。
只要能判断Redis中的播放进度是否变化即可。怎么判断呢
每当前端提交播放记录时我们可以设置一个延迟任务并保存这次提交的进度。等待20秒后因为前端每15秒提交一次20秒就是等待下一次提交检查Redis中的缓存的进度与任务中的进度是否一致。
不一致说明持续在提交无需处理一致说明是最后一次提交更新学习记录、更新课表最近学习小节和时间到数据库中
4.延迟任务方案对比
DelayQueueRedissonMQ时间轮原理JDK自带延迟队列基于阻塞队列实现。基于Redis数据结构模拟JDK的DelayQueue实现利用MQ的特性。例如RabbitMQ的死信队列时间轮算法优点不依赖第三方服务分布式系统下可用不占用JVM内存分布式系统下可以不占用JVM内存不依赖第三方服务性能优异缺点占用JVM内存只能单机使用依赖第三方服务依赖第三方服务只能单机使用
以上四种方案都可以解决问题不过本例中我们会使用DelayQueue方案。因为这种方案使用成本最低而且不依赖任何第三方服务减少了网络交互。
但缺点也很明显就是需要占用JVM内存在数据量非常大的情况下可能会有问题。但考虑到任务存储时间比较短只有20秒因此也可以接收。
如果你们的数据量非常大DelayQueue不能满足业务需求大家也可以替换为其它延迟队列方式例如Redisson、MQ等
5.Redis数据结构设计
一方面我们要缓存写数据减少写数据库频率另一方面我们要缓存播放记录减少查询数据库。因此缓存中至少要包含3个字段
记录idid用于根据id更新数据库播放进度moment用于缓存播放进度播放状态是否学完finished用于判断是否是第一次学完
课程有很多每个课程的小节也非常多。每个小节都是一个独立的KEY需要创建的KEY也会非常多浪费大量内存。可以把一个课程的多个小节作为一个KEY来缓存 6.代码实现
6.1定义延迟任务类
Data
public class DelayTaskD implements Delayed {private D data;private long deadlineNanos;public DelayTask(D data, Duration delayTime) {this.data data;this.deadlineNanos System.nanoTime() delayTime.toNanos();}Overridepublic long getDelay(TimeUnit unit) {return unit.convert(Math.max(0, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);}Overridepublic int compareTo(Delayed o) {long l getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);if(l 0){return 1;}else if(l 0){return -1;}else {return 0;}}
}6.2定义延迟任务处理类
package com.tianji.learning.utils;import com.tianji.common.utils.JsonUtils;
import com.tianji.common.utils.StringUtils;
import com.tianji.learning.domain.po.LearningLesson;
import com.tianji.learning.domain.po.LearningRecord;
import com.tianji.learning.mapper.LearningRecordMapper;
import com.tianji.learning.service.ILearningLessonService;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Objects;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.DelayQueue;Slf4j
Component
RequiredArgsConstructor
public class LearningRecordDelayTaskHandler {private final StringRedisTemplate redisTemplate;private final LearningRecordMapper recordMapper;private final ILearningLessonService lessonService;private final DelayQueueDelayTaskRecordTaskData queue new DelayQueue();private final static String RECORD_KEY_TEMPLATE learning:record:{};private static volatile boolean begin true;PostConstructpublic void init(){CompletableFuture.runAsync(this::handleDelayTask);}PreDestroypublic void destroy(){begin false;log.debug(延迟任务停止执行);}public void handleDelayTask(){while (begin) {try {// 1.获取到期的延迟任务DelayTaskRecordTaskData task queue.take();RecordTaskData data task.getData();// 2.查询Redis缓存LearningRecord record readRecordCache(data.getLessonId(), data.getSectionId());if (record null) {continue;}// 3.比较数据moment值if(!Objects.equals(data.getMoment(), record.getMoment())) {// 不一致说明用户还在持续提交播放进度放弃旧数据continue;}// 4.一致持久化播放进度数据到数据库// 4.1.更新学习记录的momentrecord.setFinished(null);recordMapper.updateById(record);// 4.2.更新课表最近学习信息LearningLesson lesson new LearningLesson();lesson.setId(data.getLessonId());lesson.setLatestSectionId(data.getSectionId());lesson.setLatestLearnTime(LocalDateTime.now());lessonService.updateById(lesson);} catch (Exception e) {log.error(处理延迟任务发生异常, e);}}}public void addLearningRecordTask(LearningRecord record){// 1.添加数据到Redis缓存writeRecordCache(record);// 2.提交延迟任务到延迟队列 DelayQueuequeue.add(new DelayTask(new RecordTaskData(record), Duration.ofSeconds(20)));}public void writeRecordCache(LearningRecord record) {log.debug(更新学习记录的缓存数据);try {// 1.数据转换String json JsonUtils.toJsonStr(new RecordCacheData(record));// 2.写入RedisString key StringUtils.format(RECORD_KEY_TEMPLATE, record.getLessonId());redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);// 3.添加缓存过期时间redisTemplate.expire(key, Duration.ofMinutes(1));} catch (Exception e) {log.error(更新学习记录缓存异常, e);}}public LearningRecord readRecordCache(Long lessonId, Long sectionId){try {// 1.读取Redis数据String key StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);Object cacheData redisTemplate.opsForHash().get(key, sectionId.toString());if (cacheData null) {return null;}// 2.数据检查和转换return JsonUtils.toBean(cacheData.toString(), LearningRecord.class);} catch (Exception e) {log.error(缓存读取异常, e);return null;}}public void cleanRecordCache(Long lessonId, Long sectionId){// 删除数据String key StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);redisTemplate.opsForHash().delete(key, sectionId.toString());}DataNoArgsConstructorprivate static class RecordCacheData{private Long id;private Integer moment;private Boolean finished;public RecordCacheData(LearningRecord record) {this.id record.getId();this.moment record.getMoment();this.finished record.getFinished();}}DataNoArgsConstructorprivate static class RecordTaskData{private Long lessonId;private Long sectionId;private Integer moment;public RecordTaskData(LearningRecord record) {this.lessonId record.getLessonId();this.sectionId record.getSectionId();this.moment record.getMoment();}}
}① 添加播放记录到Redis并添加一个延迟检测任务到DelayQueue② 查询Redis缓存中的指定小节的播放记录③ 删除Redis缓存中的指定小节的播放记录④ 异步执行DelayQueue中的延迟检测任务检测播放进度是否变化如果无变化则写入数据库