天机学堂处理高并发业务,day4保姆级

可以从三个方面来考虑

1.减少接口的响应时间

2.部署多个服务,负载均衡请求

3.sentinel服务熔断,请求控制

在我们实现的业务中,我们发现在提交学习记录的请求需要发好多次,如果并发量上来的话,会搞垮我们的服务。

大家看业务流程图


其实访问最多的业务就是更新学习记录中的不是第一次学完,更新学习小节时间的业务,其他都只是第一次

如何优化呢?

两种方案

1.利用mq异步处理

这是以前的流程

利用mq之后

接口的响应立即返回,减少了接口的响应时间

利用了mq的削峰填谷,大量的请求放到mq中,就像一个大坝一样,然后消费者只需要在自己能承受范围之内取消息,并不需要承受自己不能承受的,降低了写的频率

总结:优点:1.减少了响应时间

                      2.利用mq缓存消息,削峰填谷

                      3.降低写频率,减少数据库压力

           缺点:1.需要依靠mq的可靠性

                       2.虽然对数据库的压力变小了,只是降低了写频率,并没有减少写的次数

2.利用redis

第二种方案就是利用redis来缓存插入的数据,然后再取最后一次操作数据库

对于我们的业务要不断更新数据库中的字段来说,我们只需要最后一次写入数据库就行

总结:优点:1.写缓存速度快,减少了数据库压力

                      2.降低了数据库写频率和写次数

            缺点:1.实现复杂

                       2.依赖缓存redis可靠性

                       3.不支持事务和复杂业务

对于复杂的业务,对数据要保证一致性,redis没有回滚机制,所以这就是缺点


对于我们的业务大家再来看一下

只需要改造不是第一次学完的,更新学习记录的业务即可

思考这个业务应该采用那种方案改造呢

如果使用第一种,可以保证数据一致性,那么我们的业务仅仅是简单的更新学习记录的时间不涉及到复杂的业务,所以我们选择第二种方式

如何改造呢

思路

在进行学习记录的查询的时候,我们应该将学习记录的时刻信息写入redis,先查询redis,如果没有,然后查询数据库,也没有的话就是需要新增学习记录。

我们应该选redis数据结构呢

最先想到的是hash,用小节id作为key,value中存储键值对

实际上是不行的,多个用户有可能小节id相同,而且小节太多导致存储的太多

所以选用lesson_id作为key,value中的key为小节id,只需要增加key就行


选好了数据结构

思考业务的流程

根据finished字段判断是否是第一次学完

如果在第一次学完的时候把数据库的finished字段改为true,但是redis中的字段还是false,会导致,下一次重复小节数量加1,所以也要更新缓存,但是如果是更新缓存,在有错误的时候,触发了回滚,finished本来改为了true,回滚为false,但是redis中回滚不了,又会导致数据不一致,所以要清理redis缓存,即使回滚redis数据也被删除了,不会导致数据不一致性

更改业务

我们要更改的业务就有这四个方面

1.首先要更改的是业务1,不能频繁的修改数据库,所以改为修改缓存,然后落库

2.如果弄成定时任务,时间如果太长,会导致用户提交的时候没能保存到数据库,时间太短

会很频繁,我们可以用延迟任务,比如在提交了一个时间后,先保存在本地,然后再延迟20

s后,查看缓存中最新的数据是否和本次保存的一样,因为这个请求是每隔15s提交一次,所以

考虑到网落延迟,我们20s之后再判断是否一致,就能知道用户最后到底是不看了,还是继续观看

3.查询也是高频,会给数据库带来巨大压力,所以我们优先查询缓存,如果没有再查数据库,

数据库没有就是新增。

4.如果在第一次学完的时候把数据库的finished字段改为true,但是redis中的字段还是false,会导致,下一次重复小节数量加1,所以也要更新缓存,但是如果是更新缓存,在有错误的时候,触发了回滚,finished本来改为了true,回滚为false,但是redis中回滚不了,又会导致数据不一致,所以要清理redis缓存,即使回滚redis数据也被删除了,不会导致数据不一致性

封装的代码如下

@RequiredArgsConstructor
@Slf4j
@Component
public class LearningRecordDelayTaskHandler {

    private final StringRedisTemplate redisTemplate;
    private final static String RECORD_KEY_TEMPLATE="learning:record:{}";
    private final DelayQueue<DelayTask<RecordTaskData>> queue=new DelayQueue<>();
    private final LearningRecordMapper learningRecordMapper;
    private final LearningLessonService learningLessonService;
    public static volatile boolean begin = true;
    @PostConstruct
    public void init(){
        CompletableFuture.runAsync(this::handleDelayTask);
    }
    @PreDestroy
    public void destroy(){
        begin = false;
        log.debug("延迟任务停止!");
    }
    public void handleDelayTask(){
        while (begin){
            try {
                //1.获取到期的延迟任务
                DelayTask<RecordTaskData> 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(record.getMoment(),data.getMoment())){
                     //不一致,说明用户还在持续提交播放进度,放弃旧数据
                     continue;
                 }
                //4.一致,持久化数据到数据库
                //4.1更新学习记录的moment
                record.setFinished(null);
                learningRecordMapper.updateById(record);
                //4.2更新课表最近学习的信息
                LearningLesson learningLesson = new LearningLesson();
                learningLesson.setId(data.getLessonId());
                learningLesson.setLatestSectionId(data.getSectionId());
                learningLesson.setLatestLearnTime(LocalDateTime.now());
                learningLessonService.updateById(learningLesson);

            } catch (InterruptedException e) {
                log.error("处理延迟任务发送异常",e);
            }
        }
    }
    /*
    * 缓存到redis
    * 提交延迟任务
    * */
    public void addLearningRecordTask(LearningRecord learningRecord){
             //1.添加数据到Redis缓存
              writeRecordCache(learningRecord);
              //2.添加延迟任务到队列
              queue.add(new DelayTask<>(Duration.ofSeconds(20),new RecordTaskData(learningRecord)));
    }
    /*
    * 查询缓存,没有查数据库
    * */
    public LearningRecord readRecordCache(Long lessonId,Long sectionId){
        try {
            //1.读取redis数据
        String key= StringUtils.format(RECORD_KEY_TEMPLATE,lessonId);
        Object cache = redisTemplate.opsForHash().get(key, sectionId.toString());
        if (cache==null){
            return null;
        }

            //2.数据检查与转换
            LearningRecord record = JsonUtils.toBean(cache.toString(), LearningRecord.class);
            return record;
        } 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());
    }

    public void writeRecordCache(LearningRecord learningRecord) {
        log.info("更新学习记录缓存数据");
        try {
            //1.数据转换
            RecordCacheData recordCacheData = new RecordCacheData(learningRecord);
            String jsonStr = JsonUtils.toJsonStr(recordCacheData);
            //2.写入redis
            String key= StringUtils.format(RECORD_KEY_TEMPLATE,learningRecord.getLessonId());
            redisTemplate.opsForHash().
                    put(key,learningRecord.getSectionId().toString(),jsonStr);
            //3.添加缓存过期时间
            redisTemplate.expire(key, Duration.ofMinutes(1));
        } catch (Exception e) {
           log.error("更新学习记录缓存异常",e);
        }
    }
    /*
    * 这个类是用来存储学习记录的缓存数据
    * */
    @Data
    @NoArgsConstructor
    private static class RecordCacheData{
        private Long id;
        private Integer moment;
        private Boolean finished;

        public RecordCacheData(LearningRecord learningRecord) {
            this.id = learningRecord.getId();
            this.moment = learningRecord.getMoment();
            this.finished = learningRecord.getFinished();
        }
    }
    /*
    * 这个类是用来在延迟队列中存值,并且查询redis比较数据是否一致
    * */
    @Data
    @NoArgsConstructor
    private static class RecordTaskData{
        private Long lessonId;
        private Long sectionId;
        private Integer moment;

        public RecordTaskData(LearningRecord learningRecord) {
            this.lessonId = learningRecord.getLessonId();
            this.moment = learningRecord.getMoment();
            this.sectionId = learningRecord.getSectionId();
        }
    }
}

业务中改造的代码如下

private boolean handleVideoRecord(Long userId, LearningRecordFormDTO recordDto) {
       //1.查询旧的学习记录
        /*LearningRecord oldRecord = lambdaQuery().select().eq(LearningRecord::getUserId, userId)
                .eq(LearningRecord::getLessonId, recordDto.getLessonId())
                .eq(LearningRecord::getSectionId, recordDto.getSectionId())
                .one();*/
        /*改为查redis*/
        LearningRecord oldRecord=queryOldRecord(recordDto.getLessonId(),recordDto.getSectionId());
        //2.判断是否存在
        //3.不存在,则新增
        if (oldRecord==null){
            //3.不存在,则新增
            //3.1.转换dto为po
            LearningRecord record = BeanUtils.copyBean(recordDto, LearningRecord.class);
            record.setUserId(userId);
            record.setFinished(false);
            record.setCreateTime(recordDto.getCommitTime());
            record.setUpdateTime(recordDto.getCommitTime());
            //3.2.写入数据库
            boolean save = save(record);
            if (!save){
                throw new DbException("添加视频学习记录失败");
            }
            return false;
        }
        //4.存在,则更新
        //4.1判断是否是第一次完成
        boolean finished = !oldRecord.getFinished()&&recordDto.getMoment() * 2 >= recordDto.getDuration();
        if (!finished){
            LearningRecord record = new LearningRecord();
            record.setLessonId(recordDto.getLessonId());
            record.setSectionId(recordDto.getSectionId());
            record.setMoment(recordDto.getMoment());
            record.setId(oldRecord.getId());
            record.setFinished(oldRecord.getFinished());
            taskHandler.addLearningRecordTask(record);
            return false;
        }
        //4.2更新
        boolean success = lambdaUpdate().set(LearningRecord::getMoment, recordDto.getMoment())
                .set(finished, LearningRecord::getFinished, true)
                .set(finished, LearningRecord::getFinishTime, recordDto.getCommitTime())
                .eq(LearningRecord::getId, oldRecord.getId())
                .update();
        if (!success){
            throw new DbException("更新视频学习记录失败");
        }
        //4.3清理缓存
        taskHandler.cleanRecordCache(recordDto.getLessonId(),recordDto.getSectionId());
        return true;
    }
 /*优先缓存*/
    private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
        //1.查询缓存
        LearningRecord record = taskHandler.readRecordCache(lessonId, sectionId);

        //2.如果命中,直接返回
        if (record!=null){
            return record;
        }

        //3.未命中,查询数据库
        record = lambdaQuery().select()
                .eq(LearningRecord::getLessonId, lessonId)
                .eq(LearningRecord::getSectionId, sectionId)
                .one();
        //3.如果未命中,返回null
        if (record==null){
            return null;
        }
        //4.写入缓存
        taskHandler.writeRecordCache(record);
        return record;

    }

来自黑马程序员的天机学堂

### 天机学堂 Day3 学习内容概述 天机学堂 Day3 的学习重点围绕 **学习计划和进度管理** 展开,主要涉及如何处理用户的课程学习行为并同步更新相关数据[^1]。具体内容如下: #### 一、学习记录初始化与更新逻辑 如果用户正在观看视频类课程,则需先判断该课程是否为首次观看。此操作通过查询数据库中的学习记录实现: - 若无对应的学习记录,则判定为首次观看,并创建新的学习记录条目。 - 如果已有学习记录,则进一步更新当前观看到的具体时间点(以秒计)。这一步骤确保了系统的实时性和准确性。 #### 二、学习完成状态校验 除了基本的播放记录维护外,还需验证本次学习是否满足“学完”的条件。具体而言: - 需要确认播放进度是否达到了预设的比例阈值(通常为95%以上)。 - 同时对比之前的学习状态——若之前的标记是非完成状态而此次确实完成了全部内容,则触发一系列额外的操作来反映这一变化。 #### 三、课表动态调整功能 一旦检测到某门课程由未完成变为已完成的状态转换时,系统将自动执行以下动作之一或多个组合措施: - 更新学员个人主页上的最新学习进展展示模块; - 调整整体教学安排视图内的相应部分,使其他关联项能够及时反映出最新的情况变动。 上述流程不仅限于单一资源类型的跟踪管理,在实际应用过程中可能还会涉及到更多维度的数据交互以及更复杂的业务场景支持需求。 ```python def update_learning_status(user_id, course_id, current_second): """模拟更新学习状态""" # 查询是否有历史记录 record = get_record_by_user_and_course(user_id=user_id, course_id=course_id) if not record: create_new_record(user_id=user_id, course_id=course_id, start_time=current_second) else: last_watched_seconds = record['last_watched'] if is_completed(last_watched_seconds, total_duration_of(course_id)): mark_as_finished(user_id=user_id, course_id=course_id) adjust_curriculum_dashboard(user_id=user_id, finished=True) else: save_progress(record_id=record['id'], new_position=current_second) ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值