StrivingYu

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

补充:遇到的问题

        重启后,将虚拟机启动之后,再次访问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);
            }
        }
    }

 

posted on 2025-11-25 16:11  青云计划  阅读(12)  评论(0)    收藏  举报  来源