天机学堂 第四天 高并发优化总结
前端每隔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秒就发起一次请求,将播放记录写入数据库。 但问题是,提交播放记录的业务太复杂了,其中涉及到大量的数据库操作: 如何进行优化 单机并发能力 变同步为异步 合并写请求 提高单机并发:优化SQL,尽…...

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

前端练习<HtmlCSS>——照片墙(附完整代码及实现效果)
这个小练习也来源于b站up小K师兄,大家可以通过下面的链接学习哦~up讲的非常详细。 纯CSS写一个简单酷炫的照片墙效果~ 先看一下这个照片墙的效果: 1.鼠标没有放到图片上时,照片同比例,每张照片都有倒影的效果。 2.然…...

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

Vue element ui分页组件示例
https://andi.cn/page/621615.html...

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

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

墨烯的C语言技术栈-C语言基础-018
char c; //1byte字节 8bit比特位 int main() { int a 10; //向内存申请四个字节,存储10 &a; //取地址操作符 return 0; } 每个字节都有地址 而a的地址就是它第一个字节的地址 要先开始调试才可以查看监控和查看内存 左边是地址 中间是内存中的数据 最后面的是…...
C端与B端 - 第一弹 - 理解和区分C端与B端软件开发
作者:逍遥Sean 简介:一个主修Java的Web网站\游戏服务器后端开发者 主页:https://blog.csdn.net/Ureliable 觉得博主文章不错的话,可以三连支持一下~ 如有疑问和建议,请私信或评论留言! 前言 在软件开发领域…...
穿越多元宇宙的.NET:一场跨平台的星际旅行
概述 在软件开发的浩瀚宇宙中,.NET无疑是一颗耀眼的恒星,散发着多平台开发的光芒。从单一的.NET Framework出发,我们如今已拥有一个多元化的.NET宇宙,每个变体都是一个独特的星球,拥有自己的生态系统和生存法则。本文将…...
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浏览器的安全分析与数据提取工具,该工具可以帮助广大研究人员提取并分析Edge、Chrome和Firefox浏览器中的Cookie数据。 Cookie-Monster适用于红队和蓝队成员,能够提取WebKit主密钥,找到具…...

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

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

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

机械学习—零基础学习日志(高数16——函数极限性质)
零基础为了学人工智能,真的开始复习高数 这里我们继续学习函数极限的性质。 局部有界性 充分条件与必要条件 极限存在是函数局部有界的充分条件。什么是充分条件,什么是必要条件呢?我这里做了一点小思考,和大家分享,…...

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

angular入门基础教程(八)表单之双向绑定
绑定表单数据 为了让表单使用 Angular 的特性实现数据绑定,需要导入 FormsModule。 这个比 vue 要繁琐点,不复杂,但是比 react 的自己手动实现要方便,ng 帮我们实现了双向绑定 import { Component } from "angular/core&qu…...
【C++】C++中的find方法介绍
目录 一.find方法基本用法 1.查找字符 2.查找子字符串 3.查找子字符串(从指定位置开始) 4.查找字符范围 5.查找不包含特定字符的范围 二.使用string::npos返回无效位置 三.总结 在C中, std::string 类的 find 成员函数用于查找子字…...

多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
前端倒计时误差!
提示:记录工作中遇到的需求及解决办法 文章目录 前言一、误差从何而来?二、五大解决方案1. 动态校准法(基础版)2. Web Worker 计时3. 服务器时间同步4. Performance API 高精度计时5. 页面可见性API优化三、生产环境最佳实践四、终极解决方案架构前言 前几天听说公司某个项…...

《用户共鸣指数(E)驱动品牌大模型种草:如何抢占大模型搜索结果情感高地》
在注意力分散、内容高度同质化的时代,情感连接已成为品牌破圈的关键通道。我们在服务大量品牌客户的过程中发现,消费者对内容的“有感”程度,正日益成为影响品牌传播效率与转化率的核心变量。在生成式AI驱动的内容生成与推荐环境中࿰…...
Frozen-Flask :将 Flask 应用“冻结”为静态文件
Frozen-Flask 是一个用于将 Flask 应用“冻结”为静态文件的 Python 扩展。它的核心用途是:将一个 Flask Web 应用生成成纯静态 HTML 文件,从而可以部署到静态网站托管服务上,如 GitHub Pages、Netlify 或任何支持静态文件的网站服务器。 &am…...

BCS 2025|百度副总裁陈洋:智能体在安全领域的应用实践
6月5日,2025全球数字经济大会数字安全主论坛暨北京网络安全大会在国家会议中心隆重开幕。百度副总裁陈洋受邀出席,并作《智能体在安全领域的应用实践》主题演讲,分享了在智能体在安全领域的突破性实践。他指出,百度通过将安全能力…...

前端开发面试题总结-JavaScript篇(一)
文章目录 JavaScript高频问答一、作用域与闭包1.什么是闭包(Closure)?闭包有什么应用场景和潜在问题?2.解释 JavaScript 的作用域链(Scope Chain) 二、原型与继承3.原型链是什么?如何实现继承&a…...

关键领域软件测试的突围之路:如何破解安全与效率的平衡难题
在数字化浪潮席卷全球的今天,软件系统已成为国家关键领域的核心战斗力。不同于普通商业软件,这些承载着国家安全使命的软件系统面临着前所未有的质量挑战——如何在确保绝对安全的前提下,实现高效测试与快速迭代?这一命题正考验着…...

面向无人机海岸带生态系统监测的语义分割基准数据集
描述:海岸带生态系统的监测是维护生态平衡和可持续发展的重要任务。语义分割技术在遥感影像中的应用为海岸带生态系统的精准监测提供了有效手段。然而,目前该领域仍面临一个挑战,即缺乏公开的专门面向海岸带生态系统的语义分割基准数据集。受…...
SQL慢可能是触发了ring buffer
简介 最近在进行 postgresql 性能排查的时候,发现 PG 在某一个时间并行执行的 SQL 变得特别慢。最后通过监控监观察到并行发起得时间 buffers_alloc 就急速上升,且低水位伴随在整个慢 SQL,一直是 buferIO 的等待事件,此时也没有其他会话的争抢。SQL 虽然不是高效 SQL ,但…...

Python 实现 Web 静态服务器(HTTP 协议)
目录 一、在本地启动 HTTP 服务器1. Windows 下安装 node.js1)下载安装包2)配置环境变量3)安装镜像4)node.js 的常用命令 2. 安装 http-server 服务3. 使用 http-server 开启服务1)使用 http-server2)详解 …...