补充:遇到的问题
重启后,将虚拟机启动之后,再次访问tianji.com打开F12会发现爆CORS错误,这时可以检查虚拟机的网关微服务是否启动,没有启动可以将该服务启动,再次尝试。


播放进度记录方案优化:
1.首先定义一个Delayed类型的延迟任务类,要能保持任务数据。
package com.tianji.learning.utils;
import lombok.Data;
import java.time.Duration;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* DelayTask类是一个实现了Delayed接口的泛型类,用于表示延迟任务。
* 该类包含任务数据和任务的执行时间,并提供了获取剩余延迟时间和比较任务优先级的方法。
*
* @param <D> 泛型类型,表示任务携带的数据类型
*/
@Data // 使用Lombok的@Data注解,自动生成getter、setter等方法
public class DelayTask<D> implements Delayed {
// 任务携带的数据
private D data;
//任务的执行时间(以纳秒为单位)
private long deadlineNanos;
/**
* 构造方法,用于创建一个新的延迟任务
* @param data 任务携带的数据
* @param delayTime 延迟时间,用于计算任务的执行时间
*/
public DelayTask(D data, Duration delayTime) { //目的:调用这个类传入数据和延迟时间就行
this.data = data;
// 计算任务的执行时间:当前时间 + 延迟时间
this.deadlineNanos = System.nanoTime() + delayTime.toNanos();
}
/**
* 获取剩余的延迟时间
* @param unit 时间单位参数,用于指定返回值的时间单位
* @return 返回剩余的延迟时间,以指定的时间单位为单位
*/
@Override
public long getDelay(TimeUnit unit) { // 获取延迟时间的方法
return unit.convert(Math.max(0,deadlineNanos - System.nanoTime()),TimeUnit.NANOSECONDS);// 返回当前时间与截止时间之间的纳秒差值
}
@Override
/**
* 比较当前延迟对象与指定延迟对象的剩余延迟时间
* @param o 要比较的延迟对象
* @return 如果当前对象的延迟时间大于o,返回1;
* 如果当前对象的延迟时间小于o,返回-1;
* 如果延迟时间相等,返回0
*/
public int compareTo(Delayed o) {
// 获取当前对象和指定对象的剩余延迟时间(纳秒)并计算差值
long l = getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
// 如果差值为正,表示当前对象的延迟时间更长
if(l > 0){
return 1;
// 如果差值为负,表示当前对象的延迟时间更短
}else if(l < 0){
return -1;
// 如果差值为0,表示两者的延迟时间相等
}else {
return 0;
}
}
}
2.定义延迟任务工具类:
@Slf4j
@RequiredArgsConstructor
@Component
public class LearningRecordDelayTaskHandler {
private final StringRedisTemplate redisTemplate;
private final DelayQueue<DelayTask<RecordTaskData>> queue = new DelayQueue<>();
private final static String RECORD_KEY_TEMPLATE = "learning:record:{}";
private final LearningRecordMapper recordMapper;
private final ILearningLessonService lessonService;
private static boolean begin = true;
/**
* 初始化方法,使用@PostConstruct注解标记,在对象构造完成后自动调用
* 该方法启动一个异步任务来处理延迟任务
*/
@PostConstruct
public void init(){
// 使用CompletableFuture.runAsync方法异步执行handleDelayTask方法,异步初始化并启动延迟任务处理循环
CompletableFuture.runAsync(this::handleDelayTask);
}
@PreDestroy
public void destroy(){
begin = false;
log.info("关闭学习记录延迟任务处理");
}
public void handleDelayTask(){
while(begin){
try {
//获取到期的延迟任务
DelayTask<RecordTaskData> task = queue.take();
RecordTaskData data = task.getData();
//查询Redis缓存
LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
if (record == null) {
log.debug("没有找到学习记录的缓存数据");
continue;
}
//比较数据,mount值
if(!Objects.equals(data.getMoment(),record.getMoment())){
//不一致,说明用户还在持续提交播放进度,先不进行更新到数据库
continue;
}
//一致,说明当前用户未进行播放,持久化播放进度数据到数据库
//更新学习记录的moment
record.setFinished(null); //缓存的finished不如数据库的新,因此不更新这个
//更新学习记录
recordMapper.updateById(record);
//更新课表最近的学习记录信息
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){
//添加数据到Redis缓存
writeRecordCache(record);
//提交延迟任务到延迟队列DelayQueue
queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));
}
public void writeRecordCache(LearningRecord record) {
log.debug("更新学习记录的缓存数据");
try {
//数据转换
String json = JsonUtils.toJsonStr(new RecordCacheData(record));
//写入redis
String key = StringUtils.format(RECORD_KEY_TEMPLATE, record.getLessonId());
redisTemplate.opsForHash().put(key,record.getSectionId().toString(), json);
//添加缓存过期时间
redisTemplate.expire(key, Duration.ofMinutes(1));
} catch (Exception e) {
log.error("更新学习记录的缓存数据失败", e);
}
}
public LearningRecord readRecordCache(Long lessonId, Long sectionId){
try {
//读取redis数据
String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
Object cacheData = redisTemplate.opsForHash().get(key, sectionId.toString());
if(cacheData == null){
return null;
}
//数据的检查和转换
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());
}
@Data
@NoArgsConstructor
private 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();
}
}
@Data
@NoArgsConstructor
private 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();
}
}
}
3.修改原先处理视频的代码
1.处理视频的进度方法的改造:
private boolean handleVideoRecord(Long userId, LearningRecordFormDTO dto) {
//查询旧的学习记录
LearningRecord old = queryOldRecord(dto.getLessonId(),dto.getSectionId());
//判断是否存在学习记录
if(old == null){
//不存在学习记录则需要将当前的学习记录新增到数据库当中
LearningRecord record = BeanUtils.copyBean(dto, LearningRecord.class);
//填充数据
record.setUserId(userId);
//写入数据库
boolean success = save(record);
if(!success){
throw new DbException("新增视频记录失败");
}
//第一次提交数据为刚开始学习视频,故因此没有完成该小节的学习
return false;
}
//存在则更新,更新学习进度和finished
//判断是否是第一次完成
//判断学习进度是否超过一半了
boolean finished = !old.getFinished() && dto.getMoment() *2 >= dto.getDuration();
if (!finished){
//没有学习完该小节,将缓存写入到redis中
LearningRecord record = new LearningRecord();
record.setId(old.getId());
record.setMoment(dto.getMoment());
record.setSectionId(dto.getSectionId());
record.setLessonId(dto.getLessonId());
record.setFinished(old.getFinished());
taskHandler.addLearningRecordTask(record);
return false;
}
//完成该校节将记录写入到数据库当中
boolean success = lambdaUpdate().set(LearningRecord::getMoment, dto.getMoment())
.set(LearningRecord::getFinished, true)
.set(LearningRecord::getFinishTime, dto.getCommitTime())
.eq(LearningRecord::getId, old.getId())
.update();
if (!success) {
throw new DbException("更新学习记录失败");
}
//当数据写入到数据库当中,则需要清理redis缓存
taskHandler.cleanRecordCache(dto.getLessonId(), dto.getSectionId());
return true;
}
/**
* 查询学习记录的方法
* 首先尝试从缓存中获取记录,如果缓存未命中则查询数据库并将结果写入缓存
*
* @param lessonId 课程ID
* @param sectionId 章节ID
* @return LearningRecord 学习记录对象
*/
private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
//查询缓存,尝试获取学习记录
LearningRecord record = taskHandler.readRecordCache(lessonId, sectionId);
if (record != null){
//如果命中直接缓存将缓存返回
return record;
}
//未命中,则表示未写入缓存,则查询数据库写入缓存
record = lambdaQuery().eq(LearningRecord::getLessonId, lessonId)
.eq(LearningRecord::getSectionId, sectionId)
.one();
//写入缓存
taskHandler.writeRecordCache(record);
return record;
}
2.处理学生课程表数据
private void handleLearningLessonsChanges(LearningRecordFormDTO dto) {
//1.查询课表
LearningLesson lesson = lessonService.getById(dto.getLessonId());
if (lesson == null) {
throw new BizIllegalException("课表不存在,无法更新数据");
}
boolean allLearned = false;
//有新的完成小节,则需要查询课程数据
CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
if (cInfo == null) {
throw new BizIllegalException("课程不存在,无法更新数据");
}
//比较课程是否全部学完,已学习小节>=课程总小节数
allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();
//更新课表
LocalDateTime time = LocalDateTime.now();
lessonService.lambdaUpdate().set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED)
.setSql("learned_sections = learned_sections + 1")
.set(lesson.getLearnedSections() == 0 ,LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
.eq(LearningLesson::getId, lesson.getId())
.set(LearningLesson::getLatestLearnTime, time)
.update();
}
练习:使用线程池将延迟任务改造。
1.线程池的创建和使用:
public static void main(String[] args){
// 以下是几种常用的线程池创建方式
// Executors.newFixedThreadPool() // 创建固定线程数的线程池
// Executors.newCachedThreadPool() // 创建可缓存的线程池
// Executors.newSingleThreadExecutor() // 创建单个线程的线程池
// Executors.newScheduledThreadPool() // 创建一个定长线程池,支持定时及周期性任务执行
// 使用ThreadPoolExecutor构造器创建自定义线程池
// 参数说明:
// corePoolSize: 核心线程数,这里设置为3
// maximumPoolSize: 最大线程数,这里设置为5
// keepAliveTime: 线程空闲时间,这里设置为60秒
// unit: 时间单位,这里设置为秒
// workQueue: 工作队列,这里使用无界队列LinkedBlockingQueue
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(3, 5, 60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>());
//建议1,如果任务是属于cpu运算型的任务,推荐核心线程数为cpu的核数
//建议2.如果任务是输入io型的,推荐核心线程为cpu的两倍
poolExecutor.submit(new Runnable() {// 提交一个任务到线程池
@Override
public void run() {
}
});
}
2.将线程池引用到延迟任务当中:

public void handleDelayTask(){
while(begin){
try {
//获取到期的延迟任务
DelayTask<RecordTaskData> task = queue.take(); //阻塞方法
poolExecutor.submit(new Runnable() {
@Override
public void run() { //开启新的线程去运行
RecordTaskData data = task.getData();
//查询Redis缓存
LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
if (record == null) {//未查到数据,直接结束当前线程
log.debug("没有找到学习记录的缓存数据");
return;
}
//比较数据,mount值
if(!Objects.equals(data.getMoment(),record.getMoment())){
//不一致,说明用户还在持续提交播放进度,直接结束当前线程
return;
}
//一致,说明当前用户未进行播放,持久化播放进度数据到数据库
//更新学习记录的moment
record.setFinished(null); //缓存的finished不如数据库的新,因此不更新这个
//更新学习记录
recordMapper.updateById(record);
//更新课表最近的学习记录信息
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);
}
}
}
浙公网安备 33010602011771号