当前位置: 首页 > news >正文

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

前端每隔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();}@Overridepublic long getDelay(TimeUnit unit) {return unit.convert(Math.max(0, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);}@Overridepublic 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 {@Testvoid 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 {@Testvoid 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();}@Overridepublic long getDelay(TimeUnit unit) {return unit.convert(Math.max(0, deadlineNanos - System.nanoTime()), TimeUnit.NANOSECONDS);}@Overridepublic 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;// 项目启动后 当前类实例化 属性输入之后 方法就会运行 一般用来做初始化工作@PostConstructpublic 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.提交延迟任务到延迟队列 DelayQueuequeue.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.写入RedisString 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@NoArgsConstructorprivate 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@NoArgsConstructorprivate 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();}}
}

使用线程池来处理任务

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

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

 面试题

相关文章:

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

前端每隔15秒就发起一次请求&#xff0c;将播放记录写入数据库。 但问题是&#xff0c;提交播放记录的业务太复杂了&#xff0c;其中涉及到大量的数据库操作&#xff1a; 如何进行优化 单机并发能力 变同步为异步 合并写请求 提高单机并发&#xff1a;优化SQL&#xff0c;尽…...

Canva收购Leonardo.ai,增强生成式AI技术能力

每周跟踪AI热点新闻动向和震撼发展 想要探索生成式人工智能的前沿进展吗&#xff1f;订阅我们的简报&#xff0c;深入解析最新的技术突破、实际应用案例和未来的趋势。与全球数同行一同&#xff0c;从行业内部的深度分析和实用指南中受益。不要错过这个机会&#xff0c;成为AI领…...

前端练习<HtmlCSS>——照片墙(附完整代码及实现效果)

这个小练习也来源于b站up小K师兄&#xff0c;大家可以通过下面的链接学习哦~up讲的非常详细。 纯CSS写一个简单酷炫的照片墙效果&#xff5e; 先看一下这个照片墙的效果&#xff1a; 1.鼠标没有放到图片上时&#xff0c;照片同比例&#xff0c;每张照片都有倒影的效果。 2.然…...

PHP基于微信小程序的打车平台-计算机毕业设计源码78689

摘 要 本文介绍的是基于PHP开发的打车平台小程序。该系统旨在为用户提供一个便捷、高效的平台&#xff0c;以实现网约车的打车功能。随着社交媒体和互联网的普及&#xff0c;网约车已成为日常交通中常见的形式。然而&#xff0c;传统的打车方式存在不方便、不及时等问题。 微信…...

Vue element ui分页组件示例

https://andi.cn/page/621615.html...

redis存储结构

一、整体结构图 二、redisDb结构体 Redis是一个高性能的键值存储系统&#xff0c;它支持多种类型的数据结构&#xff0c;如字符串、列表、集合、散列等。Redis数据库由多个数据库组成&#xff0c;每个数据库用一个redisDb结构体来表示。 dict *dict; dict指向一个字典结构的指…...

SQL Server 数据误删的恢复

在日常的数据库管理中&#xff0c;数据的误删操作是难以避免的。为了确保数据的安全性和完整性&#xff0c;我们必须采取一些措施来进行数据的备份和恢复。本文将详细介绍如何在 SQL Server 中进行数据的备份和恢复操作&#xff0c;特别是在发生数据误删的情况下。假设我们已经…...

墨烯的C语言技术栈-C语言基础-018

char c; //1byte字节 8bit比特位 int main() { int a 10; //向内存申请四个字节,存储10 &a; //取地址操作符 return 0; } 每个字节都有地址 而a的地址就是它第一个字节的地址 要先开始调试才可以查看监控和查看内存 左边是地址 中间是内存中的数据 最后面的是…...

C端与B端 - 第一弹 - 理解和区分C端与B端软件开发

作者&#xff1a;逍遥Sean 简介&#xff1a;一个主修Java的Web网站\游戏服务器后端开发者 主页&#xff1a;https://blog.csdn.net/Ureliable 觉得博主文章不错的话&#xff0c;可以三连支持一下~ 如有疑问和建议&#xff0c;请私信或评论留言&#xff01; 前言 在软件开发领域…...

穿越多元宇宙的.NET:一场跨平台的星际旅行

概述 在软件开发的浩瀚宇宙中&#xff0c;.NET无疑是一颗耀眼的恒星&#xff0c;散发着多平台开发的光芒。从单一的.NET Framework出发&#xff0c;我们如今已拥有一个多元化的.NET宇宙&#xff0c;每个变体都是一个独特的星球&#xff0c;拥有自己的生态系统和生存法则。本文将…...

Python自学第五天

# 嵌套 字典嵌套字典 # 字典列表 now {pet:cat,color:black} now1 {pet:cat,color:pipe} wq [now,now1] # 这里是中括号 不是花括号 花括号打印不出来 for ff in wq:print(ff) # 创建20个外星人 打印前三个 并且显示一共创建了多少个外星人 now [] for wq in range(20):# 这…...

Cookie-Monster:一款针对Web浏览器的安全分析与数据提取工具

关于Cookie-Monster Cookie-Monster是一款针对常见Web浏览器的安全分析与数据提取工具&#xff0c;该工具可以帮助广大研究人员提取并分析Edge、Chrome和Firefox浏览器中的Cookie数据。 Cookie-Monster适用于红队和蓝队成员&#xff0c;能够提取WebKit主密钥&#xff0c;找到具…...

C语言的结构体

结构体定义 结构体指针...

C语言 写一个函数days,实现某日在本年中是第几天计算。

写一个函数days, 【定义一个结构体变量(包括年、月、日)。计算该日在本年中是第几天,注意闰年问题&#xff08;即将闰年情况包含在内&#xff09;】 由主函数将年、月、日传递给days函数,计算后将日子数传回主函数输出。 ​#include <stdio.h>typedef struct {int yea…...

2-50 基于matlab的遗传模拟退火算法的聚类算法

基于matlab的遗传模拟退火算法的聚类算法&#xff0c;以模糊K-均值聚类算法为基础&#xff0c;对各样本的聚类中心进行优化&#xff0c;输出聚类可视化结果。聚类类别数可自由输入。程序已调通&#xff0c;可直接运行。 2-50 遗传模拟退火算法的聚类算法 - 小红书 (xiaohongshu…...

电脑屏幕录制软件,分享4款(2024最新)

在今天&#xff0c;我们的电脑屏幕成为了一个多彩多姿的窗口。通过它我们可以浏览网页、观看视频、处理文档、进行游戏……有时&#xff0c;我们想要记录下这些精彩瞬间&#xff0c;与朋友分享&#xff0c;或者作为教程留存&#xff0c;这时&#xff0c;电脑屏幕录制就显得尤为…...

机械学习—零基础学习日志(高数16——函数极限性质)

零基础为了学人工智能&#xff0c;真的开始复习高数 这里我们继续学习函数极限的性质。 局部有界性 充分条件与必要条件 极限存在是函数局部有界的充分条件。什么是充分条件&#xff0c;什么是必要条件呢&#xff1f;我这里做了一点小思考&#xff0c;和大家分享&#xff0c…...

初识c++——list

一、list 1、list结构 c中list为双向带头循环列表&#xff1a; 二、list接口 1、构造 using namespace std; #include<iostream> #include<list> #include<vector> int main() {list<int> lt; //构造空的listlist<int> lt1(10, 1); //构造的l…...

angular入门基础教程(八)表单之双向绑定

绑定表单数据 为了让表单使用 Angular 的特性实现数据绑定&#xff0c;需要导入 FormsModule。 这个比 vue 要繁琐点&#xff0c;不复杂&#xff0c;但是比 react 的自己手动实现要方便&#xff0c;ng 帮我们实现了双向绑定 import { Component } from "angular/core&qu…...

【C++】C++中的find方法介绍

目录 一.find方法基本用法 1.查找字符 2.查找子字符串 3.查找子字符串&#xff08;从指定位置开始&#xff09; 4.查找字符范围 5.查找不包含特定字符的范围 二.使用string::npos返回无效位置 三.总结 在C中&#xff0c; std::string 类的 find 成员函数用于查找子字…...

Phi-4-mini-reasoning开源镜像实操:无需conda/pip,开箱即用推理环境

Phi-4-mini-reasoning开源镜像实操&#xff1a;无需conda/pip&#xff0c;开箱即用推理环境 1. 模型简介 Phi-4-mini-reasoning 是一个基于合成数据构建的轻量级开源模型&#xff0c;专注于高质量、密集推理的数据处理能力。作为Phi-4模型家族的一员&#xff0c;它经过专门微…...

Spring_couplet_generation 构建RESTful API最佳实践

Spring_couplet_generation 构建RESTful API最佳实践 最近在做一个挺有意思的小项目&#xff0c;想把一个春联生成模型包装成服务&#xff0c;方便其他应用调用。这让我重新思考了如何把一个AI模型能力&#xff0c;通过API的方式&#xff0c;既规范又稳定地提供出去。相信不少…...

Lingbot-Depth-Pretrain-ViTL-14 模型压缩与加速:面向边缘设备的部署优化教程

Lingbot-Depth-Pretrain-ViTL-14 模型压缩与加速&#xff1a;面向边缘设备的部署优化教程 想让一个像 Lingbot-Depth-Pretrain-ViTL-14 这样的大模型在树莓派、Jetson 这类小设备上跑起来&#xff0c;是不是感觉像让一头大象挤进小轿车&#xff1f;直接部署&#xff0c;设备可…...

从MATLAB到C++:手把手教你将鱼眼相机标定结果(Scaramuzza模型)部署到OpenCV项目

从MATLAB到C&#xff1a;手把手教你将鱼眼相机标定结果&#xff08;Scaramuzza模型&#xff09;部署到OpenCV项目 鱼眼相机因其超广视角在机器人导航、虚拟现实等领域应用广泛。但MATLAB标定结果如何无缝迁移到C工程环境&#xff1f;本文将彻底解决这个痛点&#xff0c;带您完成…...

cv_unet_image-colorization部署案例:RTX显卡5分钟搭建AI上色工作站

cv_unet_image-colorization部署案例&#xff1a;RTX显卡5分钟搭建AI上色工作站 1. 项目简介 你是否遇到过这样的情况&#xff1a;翻看老照片时&#xff0c;发现很多珍贵的黑白照片已经褪色发黄&#xff0c;想要恢复色彩却不知道从何下手&#xff1f;或者作为摄影师&#xff…...

技术赋能B端拓客:号码核验行业的迭代与价值升级,氪迹科技法人股东号码核验筛选,阶梯式价格

2026年&#xff0c;B端市场竞争日趋激烈&#xff0c;拓客逻辑已从“规模扩张”转向“价值深耕”&#xff0c;“精准、高效、低成本”成为所有拓客团队的核心追求。号码核验作为B端拓客的前置基础性环节&#xff0c;其服务质量直接决定线索价值、人力效能与投入回报比&#xff0…...

如何3步完成语雀文档迁移:新手终极免费指南

如何3步完成语雀文档迁移&#xff1a;新手终极免费指南 【免费下载链接】yuque-exporter export yuque to local markdown 项目地址: https://gitcode.com/gh_mirrors/yuq/yuque-exporter 还在为语雀平台策略调整而烦恼吗&#xff1f;担心自己的创作内容无处安放&#x…...

Spring AI实战系列(七):Chat Memory对话记忆实战,基于Redis实现持久化多轮对话

一、系列回顾与本篇定位1.1 系列回顾第一篇&#xff1a;完成Spring AI与阿里云百炼的基础集成&#xff0c;基于ChatModel 实现同步对话与API Key安全注入。第二篇&#xff1a;解锁ChatClient&#xff0c;实现全局统一配置与链式调用&#xff0c;告别重复样板代码。第三篇&#…...

济南精神心理专科:如何识别躯体化障碍的早期信号

济南躯体化障碍疾病就医选择难题在济南&#xff0c;面对躯体化障碍疾病的朋友最关心的是隐私和靠谱。选择一家好的医院至关重要&#xff0c;尤其是看躯体化障碍一定要选专科专业医院。这类医院不仅在专业诊疗上更有优势&#xff0c;还能提供更好的隐私保护和服务体验。本文将基…...

Mojo调用PyTorch模型推理却遭遇内存泄漏?——国家级实验室验证的4层内存隔离架构首次公开

第一章&#xff1a;Mojo调用PyTorch模型推理却遭遇内存泄漏&#xff1f;——国家级实验室验证的4层内存隔离架构首次公开在高性能AI边缘部署场景中&#xff0c;Mojo语言通过其零开销FFI机制调用PyTorch C前端&#xff08;LibTorch&#xff09;实现低延迟推理&#xff0c;但实测…...