给自己复盘的tjxt笔记day4

高并发优化方案

其中,水平扩展和服务保护侧重的是运维层面的处理。而提高单机并发能力侧重的则是业务层面的处理,也就是我们程序员在开发时可以做到的。

对于读多写少的业务,其优化手段大家都比较熟悉了,主要包括两方面:

  • 优化代码和SQL

  • 添加缓存

对于高并发写的优化方案有:

  • 优化代码及SQL

  • 变同步写为异步写

  • 合并写请求

变同步写为异步

  • 当我们接收到用户请求后,可以先不处理业务,而是发送MQ消息并返回给用户结果。

  • 而后通过消息监听器监听MQ消息,处理后续业务。

应用场景

  • 比较适合应用于业务复杂, 业务链较长,有多次数据库写操作的业务。

合并写请求

合并写请求就是指当写数据库并发较高时,不再直接写到数据库。而是先将数据缓存到Redis,然后定期将缓存中的数据批量写入数据库。

应用场景

  • 写频率较高、写业务相对简单的场景

播放进度记录方案改进

方案选择

播放进度信息,不管更新多少次,下一次续播肯定是从最后的一次播放进度开始续播。也就是说我们只需要记住最后一次即可。因此可以采用合并写方案来降低数据库写的次数和频率,而异步写做不到。

综上,提交播放进度业务虽然看起来复杂,但大多数请求的处理很简单,就是更新播放进度。并且播放进度数据是可以合并的(覆盖之前旧数据)。我们建议采用合并写请求方案

方案设计

Redis数据结构设计

既然一个课程包含多个小节,我们完全可以把一个课程的多个小节作为一个KEY来缓存,如图:

KEY

HashKey

HashValue

lessonId

sectionId:1

 

{ "id": 1, "moment": 242, "finished": true }

sectionId:2

 

{ "id": 2, "moment": 20, "finished": false }

sectionId:3

 

{ "id": 3, "moment": 121, "finished": false }

这样做有两个好处:

  • 可以大大减少需要创建的KEY的数量,减少内存占用

  • 一个课程创建一个缓存,当用户在多个视频间跳转时,整个缓存的有效期都会被延续,不会频繁的创建和销毁缓存数据

方案思路

变化后的业务具体流程为:

  • 1.提交学习记录

  • 2.判断是否是考试

    • 是:新增学习记录,并标记有小节被学完。走步骤8

    • 否:走视频流程,步骤3

  • 3.查询播放记录缓存,如果缓存不存在则查询数据库并建立缓存

  • 4.判断记录是否存在

    • 4.1.否:新增一条学习记录

    • 4.2.是:走更新学习记录流程,步骤5

  • 5.判断是否是第一次学完(进度超50%,旧的状态是未学完)

    • 5.1.否:仅仅是要更新播放进度,因此直接写入Redis并结束

    • 5.2.是:代表小节学完,走步骤6

  • 6.更新学习记录状态为已学完

  • 7.清理Redis缓存:因为学习状态变为已学完,与缓存不一致,因此这里清理掉缓存,这样下次查询时自然会更新缓存,保证数据一致。

  • 8.更新课表中已学习小节的数量+1

  • 9.判断课程的小节是否全部学完

    • 是:更新课表状态为已学完

    • 否:结束

方案优化

下一次我们再次打开该视频续播的时候,肯定是从最后一次提交的播放进度来续播。也就是说续播进度之前的N次播放进度都是没有意义的,都会被覆盖。

既然如此,我们完全没有必要定期把这些播放进度写到数据库,只需要将用户最后一次提交的播放进度写入数据库即可

因此,我们只要能判断Redis中的播放进度是否变化即可。怎么判断呢?

每当前端提交播放记录时,我们可以设置一个延迟任务并保存这次提交的进度。等待20秒后(因为前端每15秒提交一次,20秒就是等待下一次提交),检查Redis中的缓存的进度任务中的进度是否一致。

延迟任务

延迟任务的实现方案有很多,常见的有四类:

DelayQueue

Redisson

MQ

时间轮

原理

JDK自带延迟队列,基于阻塞队列实现。

基于Redis数据结构模拟JDK的DelayQueue实现

利用MQ的特性。例如RabbitMQ的死信队列

时间轮算法

优点

  • 不依赖第三方服务

  • 分布式系统下可用

  • 不占用JVM内存

  • 分布式系统下可以

  • 不占用JVM内存

  • 不依赖第三方服务

  • 性能优异

缺点

  • 占用JVM内存

  • 只能单机使用

  • 依赖第三方服务

  • 依赖第三方服务

  • 只能单机使用

使用DelayQueue方案。因为这种方案使用成本最低,而且不依赖任何第三方服务,减少了网络交互。

如果数据量非常大,DelayQueue不能满足业务需求,大家也可以替换为其它延迟队列方式,例如Redisson、MQ等

原理

DelayQueue实现了BlockingQueue接口,是一个阻塞队列。队列就是容器,用来存储东西的。DelayQueue叫做延迟队列,其中存储的就是延迟执行的任务

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

    private final transient ReentrantLock lock = new ReentrantLock();
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    
    // ... 略
}
实例

首先定义一个Delayed类型的延迟任务类,要能保持任务数据。

package com.tianji.learning.utils;

import lombok.Data;

import java.time.Duration;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

@Data
public class DelayTask<D> implements Delayed {
    private D data;
    private long deadlineNanos;

    public DelayTask(D data, Duration delayTime) {
        this.data = data;
        this.deadlineNanos = System.nanoTime() + delayTime.toNanos();
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(Math.max(0, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);
    }

    @Override
    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;
        }else {
            return 0;
        }
    }
}

接下来就可以创建延迟任务,交给延迟队列保存:

package com.tianji.learning.utils;

import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;

import java.time.Duration;
import java.util.concurrent.DelayQueue;

@Slf4j
class DelayTaskTest {
    @Test
    void testDelayQueue() throws InterruptedException {
        // 1.初始化延迟队列
        DelayQueue<DelayTask<String>> queue = new DelayQueue<>();
        // 2.向队列中添加延迟执行的任务
        log.info("开始初始化延迟任务。。。。");
        queue.add(new DelayTask<>("延迟任务3", Duration.ofSeconds(3)));
        queue.add(new DelayTask<>("延迟任务1", Duration.ofSeconds(1)));
        queue.add(new DelayTask<>("延迟任务2", Duration.ofSeconds(2)));
        // TODO 3.尝试执行任务
        
    }
}
使用

添加播放记录到Redis,并添加一个延迟检测任务到DelayQueue

    public void addLearningRecordTask(LearningRecord record){
        // 1.添加数据到Redis缓存
        writeRecordCache(record);
        // 2.提交延迟任务到延迟队列 DelayQueue
        queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));
    }

添加播放记录到Redis

  public void writeRecordCache(LearningRecord record) {
        log.debug("更新学习记录的缓存数据");
        try {
            // 1.数据转换
            String json = JsonUtils.toJsonStr(new RecordCacheData(record));
            // 2.写入Redis
            String 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);
        }
    }

异步执行DelayQueue中的延迟检测任务,检测播放进度是否变化,如果无变化则写入数据库

    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(data.getMoment(), record.getMoment())) {
                    // 不一致,说明用户还在持续提交播放进度,放弃旧数据
                    continue;
                }

                // 4.一致,持久化播放进度数据到数据库
                // 4.1.更新学习记录的moment
                record.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);
            }
        }
    }

调用的方法

查询Redis缓存中的指定小节的播放记录

    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;
        }
    }

删除Redis缓存中的指定小节的播放记录

   public void cleanRecordCache(Long lessonId, Long sectionId){
        // 删除数据
        String key = StringUtils.format(RECORD_KEY_TEMPLATE, lessonId);
        redisTemplate.opsForHash().delete(key, sectionId.toString());
    }

业务代码修改

主框架下的添加学习记录的代码的修改

 @Override
    @Transactional
    public void addLearningRecord(LearningRecordFormDTO recordDTO) {
        // 1.获取登录用户
        Long userId = UserContext.getUser();
        // 2.处理学习记录
        boolean finished = false;
        if (recordDTO.getSectionType() == SectionType.VIDEO) {
            // 2.1.处理视频
            finished = handleVideoRecord(userId, recordDTO);
        } else {
            // 2.2.处理考试
            finished = handleExamRecord(userId, recordDTO);
        }
        if (!finished) {
            // 没有新学完的小节,无需更新课表中的学习进度
            return;
        }
        // 3.处理课表数据
        handleLearningLessonsChanges(recordDTO);
    }

添加学习记录的代码

    private void handleLearningLessonsChanges(LearningRecordFormDTO recordDTO) {
        // 1.查询课表
        LearningLesson lesson = lessonService.getById(recordDTO.getLessonId());
        if (lesson == null) {
            throw new BizIllegalException("课程不存在,无法更新数据!");
        }
        // 2.判断是否有新的完成小节
        boolean allLearned = false;

        // 3.如果有新完成的小节,则需要查询课程数据
        CourseFullInfoDTO cInfo = courseClient.getCourseInfoById(lesson.getCourseId(), false, false);
        if (cInfo == null) {
            throw new BizIllegalException("课程不存在,无法更新数据!");
        }
        // 4.比较课程是否全部学完:已学习小节 >= 课程总小节
        allLearned = lesson.getLearnedSections() + 1 >= cInfo.getSectionNum();

        // 5.更新课表
        lessonService.lambdaUpdate()
                .set(lesson.getLearnedSections() == 0, LearningLesson::getStatus, LessonStatus.LEARNING.getValue())
                .set(allLearned, LearningLesson::getStatus, LessonStatus.FINISHED.getValue())
                .set(allLearned, LearningLesson::getFinishTime, LocalDateTime.now())
                .setSql("learned_sections = learned_sections + 1")
                .eq(LearningLesson::getId, lesson.getId())
                .update();
    }

对学习小节是视频的处理

        // 4.存在,则更新
        // 4.1.判断是否是第一次完成
        boolean finished = !old.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(old.getId());
            record.setFinished(old.getFinished());
            taskHandler.addLearningRecordTask(record);
            return false;
        }
        // 4.2.更新数据
        boolean success = lambdaUpdate()
                .set(LearningRecord::getMoment, recordDTO.getMoment())
                .set(LearningRecord::getFinished, true)
                .set(LearningRecord::getFinishTime, recordDTO.getCommitTime())
                .eq(LearningRecord::getId, old.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()
                .eq(LearningRecord::getLessonId, lessonId)
                .eq(LearningRecord::getSectionId, sectionId)
                .one();
        // 4.写入缓存
        taskHandler.writeRecordCache(record);
        return record;
    }

课后作业

线程池的使用

天机学堂 第四天 高并发优化总结_天机学堂 completablefuture-CSDN博客

Java线程池这部分没有学过,所以参考其他大佬的代码学习了解一下

先构造线程池

然后在执行延业务代码的时候,拉取完之后的执行让线程池里面的来执行

  • 11
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值