天机学堂2-高并发优化

news2025/1/17 1:16:09

day04-高并发优化

方案选择

实现了学习计划和学习进度的统计功能。特别是学习进度部分,为了更精确的记录用户上一次播放的进度,我们采用的方案是:前端每隔15秒就发起一次请求,将播放记录写入数据库。

在这里插入图片描述
在并发较高的情况下,会给数据库带来非常大的压力
在机器性能一定的情况下,提高单机并发能力就是要尽可能缩短业务的响应时间(ResponseTime),而对响应时间影响最大的往往是对数据库的操作。而从数据库角度来说,我们的业务无非就是读或写两种类型。

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

  • 优化代码和SQL
  • 添加缓存【如Redis内存级别缓存】

对于写多读少的业务,大家可能较少碰到,优化的手段可能也不太熟悉,这也是我们要讲解的重点。
对于高并发写的优化方案有:

  • 优化代码及SQL
  • 变同步写为异步写
  • 合并写请求:先写到Redis在写到DB,中间的不必要保存的都不写入DB,只将Redis最后一次保存到DB. 如前端每隔15秒就发起一次请求:视频播到第15秒更新moment到15、播到第30秒更新moment到30,播到第45秒更新moment到45,但我们要的是最后用户播放到的moment
    -在这里插入图片描述
    变同步为异步:
    不只是MQ一种方式,很多
    在这里插入图片描述
    合并写请求:【因为我们目的就是降低数据库的操作次数 所以采用这种方案】
    在这里插入图片描述
    写数据到缓存可以出现覆盖

方案

优化位置:
在这里插入图片描述
Redis要存的东西:User_id, 课程id 学习到的时间。因为User_id+课程id就是课表ID,所以存LessionID
在这里插入图片描述
操作redis的目的,就是更新哪一个视频哪一个小节下面学习了多少时长

public void writeRecordCache(LearningRecord record) {
    log.debug("更新学习记录的缓存数据");
    try {
        // 1.Hashvalue是JSON形式
        String json = JsonUtils.toJsonStr(new RecordCacheData(record));
        // 2.拼装Hash的key
        String key = StringUtils.format(RECORD_KEY_TEMPLATE, record.getLessonId());
        // 写入redis
        redisTemplate.opsForHash().put(key, record.getSectionId().toString(), json);
        // 3.添加缓存过期时间:1分钟
        //因为延迟任务是20s执行的,在这之前都持久化到DB了
        redisTemplate.expire(key, Duration.ofMinutes(1));
    } catch (Exception e) {
        log.error("更新学习记录缓存异常", e);
    }
}

Redis存的HashValue那个类,转成JSON的

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

新改动的地方:

在这里插入图片描述
但是少了将moment更新到课表
如何将更新的moment写入DB:

定时任务: 执行时间间隔如果是1分钟的话,当redis中moment更新后的1min才会执行,我们需求用户续播时差不超过30s. <30s执行一次这边数据库压力也很大
在这里插入图片描述
所以采用异步延迟任务 :比较moment是否一致,不一致:Redis中的moment又变了说明还在学习,不用更新到DB
在这里插入图片描述

延迟任务

在这里插入图片描述
在这里插入图片描述
DelayQueue:
在这里插入图片描述

这里我们是直接同一个线程来执行任务了。当没有任务的时候线程会被阻塞。而在实际开发中,我们会准备线程池,开启多个线程来执行队列中的任务。

代码改造

在这里插入图片描述

工具类

我们的缓存工具类就应该具备上述4个方法:

  • ① 添加播放记录到Redis,并添加一个延迟检测任务到DelayQueue
  • ② 查询Redis缓存中的指定小节的播放记录
  • ③ 删除Redis缓存中的指定小节的播放记录
  • ④ 异步执行DelayQueue中的延迟检测任务,检测播放进度是否变化,如果无变化则写入数据库
/**
 * 添加指定学习记录到redis,并提交延迟任务到延迟队列DelayQueue
 *
 * @param record 学习记录信息
 */
public void addLearningRecordTask(LearningRecord record) {
    // 1.添加数据到Redis缓存
    writeRecordCache(record);
    // 2.提交延迟任务到延迟队列 DelayQueue
    queue.add(new DelayTask<>(new RecordTaskData(record), Duration.ofSeconds(20)));//Duration延迟时间
}

RecordTaskData用于延迟任务比对moment:

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

CompletableFuture.runAsync(this::handleDelayTask) 异步并发的一个类,项目启动init()这个线程,会新开一个线程执行handleDelayTask()

	// volatile关键字:在多线程环境中,当一个线程修改了这个变量的值,其他线程能够立即看到最新的值。
    private static volatile boolean begin = true;
    // 线程池
    private static ExecutorService executor = null;


    // 项目启动后,当前类实例化创建对象 属性注入值后 该方法会运行,一般用于初始化工作
    @PostConstruct
    public void init() {
        log.info("init方法执行了");
        // 核心线程数等于CPU核心数
        Integer corePoolSize = Runtime.getRuntime().availableProcessors();
        // 创建线程池
        executor = Executors.newFixedThreadPool(corePoolSize);
        CompletableFuture.runAsync(this::handleDelayTask);
        // executor.execute(this::handleDelayTask);
    }

    // 项目销毁前后,关闭延迟队列
    @PreDestroy
    public void destroy() {
        log.debug("关闭学习记录处理的延迟任务");
        // 关闭线程池
        executor.shutdown();
        begin = false;
    }

处理延时任务

private void handleDelayTask() {
   while (begin) {
        try {
            // 1.从延迟队列尝试获取任务,poll:非阻塞方法,take非阻塞方法
            DelayTask<RecordTaskData> task = queue.take();
            executor.submit(()->{   // 线程池提取线程执行
                RecordTaskData data = task.getData();
                // 2.读取Redis缓存的学习记录
                LearningRecord record = readRecordCache(data.getLessonId(), data.getSectionId());
                log.debug("获取到要处理的播放记录任务,任务数据:{},缓存数据:{}", task.getData(), record);
                if (record == null) {
                    return;
                }
                // 3.比较新提交的延迟任务的视频播放进度数值和redis缓存中的是否一致
                if (!Objects.equals(data.getMoment(), record.getMoment())) {
                    // 4.如果不一致,播放进度在变化,无需持久化
                    return;
                }
                // 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);
        }
    }
}

学习记录流程改造

Step2: 查询指定学习记录是否已存在,先从redis中查询,redis没命中则从数据库中查询

private LearningRecord queryOldRecord(Long lessonId, Long sectionId) {
	  // 查询redis缓存
	  LearningRecord cacheRecord = taskHandler.readRecordCache(lessonId, sectionId);
	  // redis缓存命中,直接返回
	  if (cacheRecord != null) {
	      return cacheRecord;
	  }
	  // redis缓存未命中,查询数据库
	  LearningRecord dbRecord = this.lambdaQuery().eq(LearningRecord::getLessonId, lessonId)
	          .eq(LearningRecord::getSectionId, sectionId).one();
	  // 数据库查询结果为null,表示记录不存在,需要新增学习记录,返回null即可
	  if (dbRecord == null) {
	      return null;
	  }
	  // 数据库查询结果写入redis缓存
	  taskHandler.writeRecordCache(dbRecord);
	  return dbRecord;
}

Step1+4:

// 判断本小节是否是首次完成:之前未完成且视频播放进度大于50%
boolean isFinished = !oldRecord.getFinished() && dto.getMoment() * 2 >= dto.getDuration();
// 更新学习记录,根据学习记录LearningRecord主键id进行匹配
if (!isFinished) {
    LearningRecord record = LearningRecord.builder()
            .id(oldRecord.getId())
            .lessonId(dto.getLessonId())
            .sectionId(dto.getSectionId())
            .finished(oldRecord.getFinished())
            .moment(dto.getMoment())
            .build();
    // 添加指定学习记录到redis,并提交延迟任务到延迟队列DelayQueue
    taskHandler.addLearningRecordTask(record);
    // 返回,本小节未完成
    return false;
}

清理redis相应record

taskHandler.cleanRecordCache(dto.getLessonId(), dto.getSectionId());

首次完成视频播放,可以增加积分,发送MQ消息实现

 if (isFinished) {
     // 发送MQ消息实现观看学习视频获取积分
     rabbitMqHelper.send(MqConstants.Exchange.LEARNING_EXCHANGE,
             MqConstants.Key.LEARN_SECTION,
             SignInMessage.of(userId,10));   // 学习一个视频 + 10积分
 }

线程池

在这里插入图片描述
Executors.newxxx()没有指定队列的capacity可能会OOM(queue方的任务太多导致内存溢出),阿里规范禁用这种,用下面显式new ThreadPoolExecutor()

在这里插入图片描述
线程池的运行流程:
在这里插入图片描述

  1. 先看有无空闲的核心线程,若有执行任务,没有不是直接用临时线程,
  2. 而是把任务放到阻塞队列排队,核心线程从队列中取take;
  3. 当阻塞队列满了,才看有无可用临时线程;没有的话,创建临时线程,
  4. 若临时线程达到上限,拒绝。

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

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

相关文章

ROS2 准备工作(虚拟机安装,Ubuntu安装,ROS2系统安装)

准备工作 虚拟机安装 大家可以自行去安装VMware链接&#xff1a;https://pan.baidu.com/s/1KcN1I9FN--Sp1bUsjKqWVA?pwd6666 提取码&#xff1a;6666(提供者&#xff1a;零基础编程入门教程) 教程&#xff1a;【【2025最新版】VMware虚拟机安装教程&#xff0c;手把手教你免…

在一个地方待多久才会改变ip属地

‌在当今数字化时代&#xff0c;IP地址作为网络世界的“门牌号”&#xff0c;不仅承载着设备连接互联网的身份信息&#xff0c;还常常与地理位置相关联。随着人们频繁地迁徙、旅行或在不同地点工作&#xff0c;一个自然而然的问题浮现在许多人心头&#xff1a;究竟在一个地方待…

CCLINKIE转ModbusTCP网关,助机器人“掀起”工业智能的“惊涛骇浪”

以下是一个稳联技术CCLINKIE转ModbusTCP网关&#xff08;WL-CCL-MTCP&#xff09;连接三菱PLC与机器人的配置案例&#xff1a;设备与软件准备设备&#xff1a;稳联技术WL-CCL-MTCP网关、三菱FX5UPLC、支持ModbusTCP协议的机器人、网线等。 稳联技术ModbusTCP转CCLINKIE网关&…

QT在 MacOS X上,如何检测点击程序坞中的Dock图标

最近在开发MacOS的qt应用&#xff0c;在做到最小化系统托盘功能时&#xff0c;发现关闭窗口后再次点击程序坞中的Dock图标不能将主界面再显示出来。查询里很多资料&#xff0c;发现是QT自身的问题&#xff0c;没有做相关的点击Dock图标的处理。 于是我参考了国内和国外的这两篇…

langchain4j执行源码分析

要做大模型应用&#xff0c;不可避免会接触到langchain&#xff0c;但是langchain本身使用py实现&#xff0c;对于java用户上手体验不是很友好。继而出现了java版的langchain&#xff0c;即langchain-4j。这里我们用脑图分析一下其执行源码。

【案例81】NMC调用导致数据库的效率问题

问题现象 客户在使用NC系统时&#xff0c;发现系统特别卡顿。需要紧急排查。 问题分析 排查NMC发现&#xff0c;所有的线程都处于执行SQL层面&#xff0c;说明数据库当前出现了异常。查看数据库资源状态发现&#xff0c;Oracle相关进程CPU利用率达到了100%。 查看现在数据库…

PyTorch框架——基于深度学习YOLOv5神经网络水果蔬菜检测识别系统

基于深度学习YOLOv5神经网络水果蔬菜检测识别系统&#xff0c;其能识别的水果蔬菜有15种&#xff0c;# 水果的种类 names: [黑葡萄, 绿葡萄, 樱桃, 西瓜, 龙眼, 香蕉, 芒果, 菠萝, 柚子, 草莓, 苹果, 柑橘, 火龙果, 梨子, 花生, 黄瓜, 土豆, 大蒜, 茄子, 白萝卜, 辣椒, 胡萝卜,…

DFT可测性设置与Tetramax测试笔记

1 DFT 1.1 DFT类型 1、扫描链&#xff08;SCAN&#xff09;&#xff1a; 扫描路径法是一种针对时序电路芯片的DFT方案.其基本原理是时序电路可以模型化为一个组合电路网络和带触发器(Flip-Flop&#xff0c;简称FF)的时序电路网络的反馈。 Scan 包括两个步骤&#xff0c;scan…

分布式ID的实现方案

1. 什么是分布式ID ​ 对于低访问量的系统来说&#xff0c;无需对数据库进行分库分表&#xff0c;单库单表完全可以应对&#xff0c;但是随着系统访问量的上升&#xff0c;单表单库的访问压力逐渐增大&#xff0c;这时候就需要采用分库分表的方案&#xff0c;来缓解压力。 ​…

28.找出字符串中第一个匹配项的下标【力扣】KMP前缀表 ≈ find() 函数、暴力解法

class Solution { public: //得到前缀表void getNext(int *next,string needle){int j0;for(int i1;i<needle.size();i){while(j>0 && needle[j]!needle[i]) jnext[j-1];//**j>0**>j0是出口if(needle[i]needle[j]) j;next[i]j;//若写入if中&#xff0c;则该…

当自动包布机遇上Profinet转ModbusTCP网关,“妙啊”,工业智能“前景无限

在自动化控制技术日新月异的当下&#xff0c;Profinet与ModbusTCP这两种协议在工业通信领域占据着举足轻重的地位。ModbusTCP是基于以太网的串行通信协议&#xff0c;而Profinet则是依托工业以太网的现场总线协议。它们在数据传输速度、实时性表现以及兼容性等方面各具特色。不…

ADC(Analog-to-digital converter)模拟-数字转换器

ADC简介 ADC&#xff08;Analog-to-Digital Converter&#xff09;&#xff0c;即模拟-数字转换器&#xff0c;是一种将模拟信号转换成数字信号的电子设备。它在现代电子系统中扮演着至关重要的角色&#xff0c;广泛应用于传感器信号处理、通信系统、医疗设备、工业自动化等多…

Uniapp判断设备是安卓还是 iOS,并调用不同的方法

在 UniApp 中&#xff0c;可以通过 uni.getSystemInfoSync() 方法来获取设备信息&#xff0c;然后根据系统类型判断当前设备是安卓还是 iOS&#xff0c;并调用不同的方法。 示例代码 export default {onLoad() {this.checkPlatform();},methods: {checkPlatform() {// 获取系…

TP4056锂电池充放电芯片教程文章详解·内置驱动电路资源!!!

目录 TP4056工作原理 TP4056引脚详解 TP4056驱动电路图 锂电池充放电板子绘制 编写不易&#xff0c;仅供学习&#xff0c;感谢理解。 TP4056工作原理 TP4056是专门为单节锂电池或锂聚合物电池设计的线性充电器&#xff0c;充电电流可以用外部电阻设定&#xff0c;最大充电…

平滑算法 效果比较

目录 高斯平滑 效果对比 移动平均效果比较: 高斯平滑 效果对比 右边两个参数是1.5 2 代码: smooth_demo.py import numpy as np import cv2 from scipy.ndimage import gaussian_filter1ddef gaussian_smooth_array(arr, sigma):smoothed_arr = gaussian_filter1d(arr, s…

Jenkins-简介/安装!

一. 关于持续集成&#xff1a; 持续集成(CI ) [ Continuous Integration ]&#xff0c;通俗来讲&#xff0c;就是一个能监控版本控制系统变化的工具&#xff0c;可以自动编译和测试集成的应用程序。出现问题&#xff0c;能够及时的通知相应人员。持续集成是一种思维工具集&…

Flutter中Get.snackbar避免重复显示的实现

在pubspec.yaml中引入依赖框架。 #GetX依赖注解get: ^4.6.5创建一个SnackBarManager管理类去管理每个提示框。 import package:get/get.dart; import package:flutter/material.dart;class SnackBarManager {factory SnackBarManager() > instance;static final SnackBarMa…

c#删除文件和目录到回收站

之前在c上遇到过这个问题&#xff0c;折腾许久才解决了&#xff0c;这次在c#上再次遇到这个问题&#xff0c;不过似乎容易了一些&#xff0c;亲测代码如下&#xff0c;两种删除方式都写在代码中了。 直接上完整代码&#xff1a; using Microsoft.VisualBasic.FileIO; using Sy…

微信小程序集成Vant Weapp移动端开发的框架

什么是Vant Weapp Vant 是一个轻量、可靠的移动端组件库&#xff0c;于 2017 年开源。 目前 Vant 官方提供了 Vue 2 版本、Vue 3 版本和微信小程序版本&#xff0c;并由社区团队维护 React 版本和支付宝小程序版本。 官网地睛&#xff1a;介绍 - Vant Weapp (vant-ui.gith…

kafka原理和实践

Kafka是当前分布式系统中最流行的消息中间件之一&#xff0c;凭借着其高吞吐量的设计&#xff0c;在日志收集系统和消息系统的应用场景中深得开发者喜爱。本篇就聊聊Kafka相关的一些知识点。主要包括以下内容&#xff1a; Kafka简介 Kafka特点Kafka基本概念Kafka架构Kafka的几…