天机学堂 第四天 高并发优化总结

news2024/11/24 2:41:43

前端每隔15秒就发起一次请求,将播放记录写入数据库。

但问题是,提交播放记录的业务太复杂了,其中涉及到大量的数据库操作:

如何进行优化

单机并发能力 变同步为异步 合并写请求

提高单机并发:优化SQL,尽量走索引,避免双重for循环,添加缓存

 提高单机并发能力

变同步为异步

合并写请求

合并写请求方案其实是参考高并发读的优化思路:当读数据库并发较高时,我们可以把数据缓存到Redis,这样就无需访问数据库,大大减少数据库压力,减少响应时间。

既然读数据可以建立缓存,那么写数据可以不可以也缓存到Redis呢?

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

提交学习记录业务优化

记录是否已经存在放入缓存中(因为每次提交都会去数据库查询),更新学习记录时间放入缓存中

对提交学习记录进行改造,每隔15秒进行一次提交对数据库压力太大,考虑到只有最后一次提交才有效,所以我们对存在数据库操作的地方进行优化

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

 如何设计缓存字段?

用户学习视频的过程中,可能会在多个视频之间来回跳转,这就会导致频繁的创建缓存、缓存过期,影响到最终的业务性能。该如何解决呢?

使用hash key解决

实际操作中可以直接把实体类转化为JSON 当做value存入

 但是存在一定问题

但问题来了,我们怎么知道哪一次提交是最后一次提交呢?

只要用户一直在提交记录,Redis中的播放进度就会一直变化。如果Redis中的播放进度不变,肯定是停止了播放,是最后一次提交。

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

核心思想

每当前端提交播放记录时,我们可以设置一个延迟任务并保存这次提交的进度。等待20秒后(因为前端每15秒提交一次,20秒就是等待下一次提交),检查Redis中的缓存的进度与任务中的进度是否一致。(把数据缓存到redis中,同时设置一个20秒的延迟任务,20秒后执行这个任务,执行这个任务的时候再一次跟redis中的时间比对,如果一样则更新数据库,否则跳过

  • 不一致:说明持续在提交,无需处理

  • 一致:说明是最后一次提交(暂停了或者离开播放了),(提交延迟任务)更新学习记录、更新课表最近学习小节和时间到数据库中

 延迟任务方案

DelayQueue的用法 

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.尝试执行任务
        
    }
}

执行任务

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)));
        // 3.尝试执行任务
        while (true) {
            DelayTask<String> task = queue.take();
            log.info("开始执行延迟任务:{}", task.getData());
        }
    }
}

 

 开始改造

封装的工具类

延迟队列里面放的就是这一个个DelayTask<T>

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

 需要开启另一个线程来执行任务

@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 volatile boolean begin = true;
    // 项目启动后 当前类实例化 属性输入之后 方法就会运行 一般用来做初始化工作
    @PostConstruct
    public void init(){
        CompletableFuture.runAsync(this::handleDelayTask);
        log.debug("开启 新线程执行handleDelayTask方法");// 开启 新线程执行handleDelayTask方法
    }
    @PreDestroy  // 当前类是实例 销毁之前该方法执行
    public void destroy(){
        log.debug("关闭学习记录处理的延迟任务");
        begin = false;
    }
    private void handleDelayTask(){
        while (begin){
            try {
                // 1.尝试获取任务   take是阻塞方法
                DelayTask<RecordTaskData> task = queue.take();
                log.debug("获取到要处理的播放记录任务");
                RecordTaskData data = task.getData();
                // 2.读取Redis缓存
                LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
                log.debug("获取到要处理的播放记录任务,任务数据{}  缓存中的数据{}",data,record);
                if (record == null) {
                    continue;
                }
                // 3.比较数据
                if(!Objects.equals(data.getMoment(), record.getMoment())){
                    // 4.如果不一致,播放进度在变化,无需持久化
                    continue;
                }
                // 5.如果一致,证明用户离开了视频,需要持久化
                // 5.1.更新学习记录
                record.setFinished(null);
                recordMapper.updateById(record);
                // 5.2.更新课表
                LearningLesson lesson = new LearningLesson();
                lesson.setId(data.getLessonId());
                lesson.setLatestSectionId(data.getSectionId());
                lesson.setLatestLearnTime(LocalDateTime.now());
                lessonService.updateById(lesson);

                log.debug("准备持久化学习记录信息");
            } catch (Exception e) {
                log.error("处理播放记录任务发生异常", e);
            }
        }
    }

    public void addLearningRecordTask(LearningRecord record){
        // 1.添加数据到Redis缓存
        writeRecordCache(record);
        // 2.提交延迟任务到延迟队列 DelayQueue
        queue.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.写入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);
        }
    }

    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());
    }
    // 缓存实体类
    @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();
        }
    }
}

使用线程池来处理任务

目前我们的延迟任务执行还是单线程模式,大家将其改造为线程池模式,

 拉取方法 还是使用哪个开辟的线程去拉取,但是拉取完之后的执行让线程池里面的来执行

 面试题

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1972850.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

vscode中WSL插件的安装配置

1.安装WSL插件 2.点击左下角的蓝色区域&#xff0c;Connect to WSL连接下载Linux系统&#xff0c;我这里下载Ubuntu ![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/7ec21d81b4ec49f3856be2d8013bc12f.png 3.输入用户名和密码 4.成功连接 5.配置过程中出现的问题 …

LinuxUNIX系统编程手册——(十四)系统编程概念

14.1 设备专用文件&#xff08;设备文件&#xff09; 设备专用文件与系统的某个设备相对应。在内核中&#xff0c;每种设备类型都有与之相对应的设备驱动程序&#xff0c;用来处理设备的所有 I/O 请求。设备驱动程序属内核代码单元&#xff0c;可执行一系列操作&#xff0c;&a…

服务器SSL证书年费一般多少钱?

SSL证书作为网络安全的重要组成部分&#xff0c;不仅能够保护数据传输的安全性&#xff0c;还能增强用户对网站的信任度。然而&#xff0c;面对市场上众多的SSL证书选项&#xff0c;许多人可能会感到困惑&#xff1a;究竟应该选择哪种类型的SSL证书&#xff1f;它们的价格又是怎…

【从零开始一步步学习VSOA开发】搭建VSOA运行环境

搭建VSOA运行环境 为方便 VSOA 的运行和调测&#xff0c;这里选择RealEvo-Simulator 下的 AMD64 平台作为 VSOA 的硬件运行环境&#xff0c;操作系统则选择标准版的 SylixOS 而不是容器版的SylixOS。 下载虚拟机资源 RealEvo-Simulator 并不自带 AMD64 平台虚拟机&#xff0…

开发板与ubuntu不能ping通怎么办?

TOC 第一步&#xff1a;VMware 设置 打开 VMware Workstation Pro 里的 虚拟机 -> 设置 设置网络适配器为桥接模式。这里不要勾选“复制物理网络连接状态”。 因为电脑是 WiFi 上网&#xff0c;所以需要添加一个网络适配器并设置成 NAT 模式&#xff0c;供虚拟机上网。具…

19066 第K小子串

这个问题可以通过使用集合&#xff08;set&#xff09;和优先队列&#xff08;priority_queue&#xff09;来解决。我们首先遍历字符串的所有子串&#xff0c;然后将这些子串放入一个集合中&#xff0c;这样可以去除重复的子串。然后我们将集合中的子串放入一个优先队列中&…

jvm方法调用指令invokestatic,invokespecial,invokeinterface,invokevirutal分析

写在前面 本文来看下jvm方法调用相关的4个指令invokestatic,invokespecial,invokeinterface,invokevirutal。 1&#xff1a;如何来记 1.1&#xff1a;静态绑定和动态绑定 如果是在编译期就能确定要调用的方法&#xff0c;就叫做静态绑定&#xff0c;比如构造函数方法&#…

Redis RDB AOF持久化 主从集群同步原理

RDB RDB Redis数据备份文件 也被叫做Redis数据快照 简单来说就是 把内存中的所有数据记录到磁盘中 当Redis实例故障实例重启后从磁盘读取快照文件恢复数据 快照文件称为RDB文件 默认时保存在当前运行目录执行时机 执行save命令 127.0.0.1:6379> save OK 127.0.0.1:6379&g…

(002)两数相加

思路分析&#xff1a; 类似于一个大数加法&#xff0c;注意链表的边界和进位&#xff0c;同时还有注意存在两个链表长度不等&#xff0c;需要处理多出来的元素 代码实现&#xff1a; /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNod…

PDF发票解析并将信息回填到前端(1)后端解析PDF

文章目录 参考文章技术栈需求解析发票类型 1. 最终项目结构1.1 说明 2. 相关代码2.1 导入相应的maven依赖2.2 实体类2.3 工具类2.4 三层架构controllerservicemapper 参考文章 参考文章 技术栈 SpringBootVue 需求 本文主要是实现提取发票中的部分内容&#xff0c;并实现自…

单调栈② | Java | LeetCode 接雨水 最大的矩形

42. 接雨水 暴力法 for循环遍历每一个柱子&#xff0c;内层for循环找到左边和右边比它高的柱子 时间复杂度 n^2 优化&#xff1a;添加一个预处理 定义一个数组&#xff0c;存放该柱子右边比他高的柱子是哪一个 再用一个数组&#xff0c;存放该柱子左边比他高的柱子是哪一个 …

html页面下载及多html文件合成单pdf方法

1&#xff0c;html页面下载 &#xff5e;1&#xff0c;首先在edge中微软扩展商店搜索 “SingleFile”添加扩展 浏览器右上角点击扩展按钮 下拉菜单选择管理扩展 点击 获取 Microsoft Edge 扩展 进入微软商店搜索 “singlefile” 点击获取 点击浏览器右上角扩展按钮&#xff0c;…

IPv4 vs IPv6:了解这两大协议的关键差异

我发现&#xff0c;很多找代理IP的朋友在后台问我的问题都很相似&#xff01;都被配置IP的最后一步&#xff1a;选择IPv4还是IPv6&#xff1f;给难住了。昨晚我一晚没睡&#xff0c;终于整理出对IPv4和IPv6的对比总结&#xff0c;从概念阐述到特点对比&#xff0c;再解答IPv6总…

利用 IP 地址进行社交工程攻击?

社交工程攻击是网络安全的主要威胁之一。现在攻击者不再仅依赖技术漏洞&#xff0c;而是想利用人性的弱点来获取有价值的信息或实现非法目的。 IP 地址在社交工程攻击中的作用 定位和伪装获取目标用户的 IP 地址&#xff0c;大致确定目标用户地理位置&#xff0c;然后伪装成当…

spring原理(自学第六天)

Aware 接口及 InitializingBean 接口 今天将会学到Aware 接口及 InitializingBean 接口 我们可以先了解他们的作用&#xff1a; 1. Aware 接口用于注入一些与容器相关信息, 例如 a. BeanNameAware 注入 bean 的名字 b. BeanFactoryAware 注入…

解锁LLM应用潜能:提示工程的39种方法与应用全解析!

大型语言模型&#xff08;LLMs&#xff09;在许多不同的自然语言处理&#xff08;NLP&#xff09;任务上表现出了显著的性能。提示工程在提升LLMs已有能力方面发挥着关键作用&#xff0c;使其在各种NLP任务上取得了显著的性能提升。提示工程需要编写自然语言指令&#xff0c;即…

QT 应用程序输出中文乱码

一 &#xff0c;选择文本编码 1. 点击编辑再点击Select Encoding选择编码 2 .在弹出的窗口&#xff0c;选择UTF-8再点击按编码保存即可 3. 重新编译&#xff0c;可以发现中文乱码问题解决

Spring源码解析(28)之AOP的核心对象创建过程总结

一、总结图 二、总结 以上是对AOP核心对象创建的一个总结&#xff0c;接下来我们分点介绍我们自从定义了aop.xml之后&#xff0c;spring在启动的时候是怎么创建了这些核心对象的。 启动spring容器之后&#xff0c;其实核心是是refresh方法&#xff0c;refresh的13个核心方法&…

CTFHub——XSS——反射型

1、反射型&#xff1a; 发现为表单式&#xff0c;猜测哪个可能存在注入漏洞&#xff0c;分别做测试注入发现name框存在xss漏洞 输入发现有回显但不是对方cookie&#xff0c;参考wp发现要用xss线上平台 将xss平台测试语句注入&#xff0c;将得到的url编码地址填入url框&#xf…

Lineage凌骥电源模块维修CP2725AC54Z

Lineage Power交换机电源常见维修型号有&#xff1a; CAR2024FPZ0-1A、CP2725AC54TEZ、AXA005A0X-SRZ、APXW005A0X3-SRZ、AXA016A0X3-SRZ、EP3000AC48TE、CP3000AC54TEZ、 CP3000AC54TEZ、QBDW033A0B641Z及EP4848系列直流电源维修等。 凌骥电源维修实践中&#xff0c;有许多开关…