【开发篇】五、文章内容审核接口的内存问题优化

news2024/12/24 8:11:38

文章目录

  • 1、初始实现思路:@Async注解新开一个线程去审核
  • 2、改进思路一:加LinkedBlockingQueue阻塞队列
  • 3、改进思路二:RabbitMQ
  • 4、总结

背景:文章微服务中有一个文章审核接口,接口内又调用阿里云的内容安全接口进行文字图片的审核

在这里插入图片描述

1、初始实现思路:@Async注解新开一个线程去审核

客户端请求经网关到文章微服务后,因为调用阿里云安全审核接口得等一会儿才能出结果,所以这里做个落库后即返回信息给用户(比如提交成功,审核中),然后异步去调审核接口。实现思路:用@Async注解单起一个线程去审核,接口当前线程直接返回结果。

在这里插入图片描述

Demo代码:

@PostMapping("/demo1/{id}")
public void article1(@PathVariable("id") long id, @RequestBody ArticleDto article){
    article.setId(id);
    articleService.asyncSaveArticle(article);  
}

@Service
public class ArticleServiceImpl implements ArticleService {

    @Autowired
    private AliyunUtil aliyunUtil;

    @Override
    @Async("asyncTaskExecutor")
    public void asyncSaveArticle(ArticleDto article) {
        //调用阿里云审核接口,其余落库操作的代码略
        aliyunUtil.verify(article.getTitle() + "。" + article.getContent()); //拼接标题和内容成一个新字符串来审核
    }
}

这里用sleep模拟远程调用阿里云审核接口的延迟:

@Component
public class AliyunUtil {

    //阿里云审核接口调用
    public void verify(String content){
        try {
            /**
             *  调用第三方接口审核数据,但是此时网络出现问题,
             * 第三方接口长时间没有响应,此处使用休眠来模拟30秒
             */
            Thread.sleep(30 * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

线程池定义,设置核心线程数、最大线程数、缓存队列长度。注意其中的最大线程数为Integer.MAX_VALUE:

@Configuration
@EnableAsync   //开启异步调用@Async
public class AsyncThreadPoolTaskConfig {

    private static final int corePoolSize = 50;       		// 核心线程数(默认线程数)
    private static final int keepAliveTime = 10;			// 允许线程空闲时间(单位:默认为秒)
    private static final int queueCapacity = 200;			// 缓冲队列数
    private static final String threadNamePrefix = "Async-Task-"; // 线程池名前缀
 
    @Bean("asyncTaskExecutor")
    public ThreadPoolTaskExecutor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize); 
        executor.setMaxPoolSize(Integer.MAX_VALUE);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        // 线程池对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

尝试压测这个接口,压测前VisualVM下堆内存平稳且占用较少:

在这里插入图片描述

压测,Jmeter开50个线程不停并发调审核接口:

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

可以看到堆占用呈上涨趋势,不到两分钟就接近一个G:
在这里插入图片描述
查看线程信息:六万多线程对象

在这里插入图片描述

很明显,线程池不合理,修改最大线程数为100:

在这里插入图片描述

重启,堆内存右上右下,整体在一个区间内:
在这里插入图片描述

线程对象数量也只有100个左右:

在这里插入图片描述

但这样很多请求就被丢弃了(线程池这个办事大厅,就这么多窗口和排队座位,根据我设置的拒绝策略,没地儿了就拒绝并抛出异常)

在这里插入图片描述

很明显,问题的根源在于异步调用阿里云审核接口慢,而请求一直在过来,生产的快,消费的慢。而现在的实现,有两个弊病:

  • 给@Async用的线程池,线程数开大了或者等待队列长度调大了,会导致大量线程对象创建,占用堆内存
  • 任务没有持久化,一旦宕机或者走了线程池拒绝策略,就会丢任务

2、改进思路一:加LinkedBlockingQueue阻塞队列

在这里插入图片描述

  • 关于丢任务的解决:Service层加个写库操作,把文章数据写库里
  • 关于大量线程对象被创建的解决:中间垫一个LinkedBlockingQueue阻塞队列(默认大小Intege.MAX_VALUE)

调审核接口,文章数据写库 + 文章数据放入队列,不停从阻塞队列中取数据,提交异步审核任务到一个固定线程数的线程池。Demo代码:

@PostMapping("/demo2/{id}")
public void article2(@PathVariable("id") long id, @RequestBody ArticleDto article){
    article.setId(id);
    articleService.saveArticle(article);
}
@Service
public class ArticleServiceImpl implements ArticleService {

    @Override
    @Async("asyncTaskExecutor")
    public void asyncSaveArticle(ArticleDto article) {
        System.out.println(Thread.currentThread().getName());
        aliyunUtil.verify(article.getTitle() + "。" + article.getContent());
    }
}

这次的线程池就固定核心线程数50个:

@Configuration
@EnableAsync
public class ThreadPoolTaskConfig  {

    public static final BlockingQueue<ArticleDto> BUFFER_QUEUE = new LinkedBlockingQueue<>();
    private static final int corePoolSize = 50;       		// 核心线程数(默认线程数)
    private static final int maxPoolSize = 100;			    // 最大线程数
    private static final int keepAliveTime = 10;			// 允许线程空闲时间(单位:默认为秒)
    private static final int queueCapacity = 200;			// 缓冲队列数
    private static final String threadNamePrefix = "Async-Service-"; // 线程池名前缀
 
    @Bean("taskExecutor")
    public ThreadPoolTaskExecutor getAsyncExecutor(){
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(corePoolSize);
        executor.setMaxPoolSize(Integer.MAX_VALUE);
        //executor.setMaxPoolSize(maxPoolSize);
        executor.setQueueCapacity(queueCapacity);
        executor.setKeepAliveSeconds(keepAliveTime);
        executor.setThreadNamePrefix(threadNamePrefix);
        // 线程池对拒绝任务的处理策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        // 初始化
        executor.initialize();
        return executor;
    }
}

拿一个@PostConstruct注解,目的是执行下从队列取数据提交审核任务到线程池的操作:

@Component
public class ArticleSaveTask {
    @Autowired
    @Qualifier("taskExecutor")
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;

    @PostConstruct
    public void pullArticleTask(){
        /**
         * 向线程池提交50个任务
         * 每个任务是一个死循环,不停的从我放入文章对象的队列中拿数据,掉审核接口(这里的审核接口直接用休眠模拟)
         */
        for (int i = 0; i < 50; i++) {
            threadPoolTaskExecutor.submit((Runnable) () -> {
                while (true){
                    try {
                        ArticleDto data = BUFFER_QUEUE.take();
                        /**
                         * 获取到队列中的数据之后,调用第三方接口审核数据,但是此时网络出现问题,
                         * 第三方接口长时间没有响应,此处使用休眠来模式30秒
                         */
                        Thread.sleep(30 * 1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }
}

启动,同样的jmeter脚本压测:

在这里插入图片描述

看到堆内存一路升高,且控制台发现已经往队列放了34万个对象了:

在这里插入图片描述

还是不合理,审核时间久,对阻塞队列,生产的快,消费的慢,大量对象积压,最终会oom。考虑把阻塞队列的容量从默认的Intege.MAX_VALUE调小,比如2000,此时内存不会oom,但容量到2000后再add会抛异常,还得自己实现持久化:

public static final BlockingQueue<ArticleDto> BUFFER_QUEUE = new LinkedBlockingQueue<>(2000);

比如当容量满的时候,先存到数据库,等队列不满了再从库里拿出来放到队列(那这里肯定不能用add了,add是满时再放抛异常Queue full,可用put方法)以保证数据不丢失,如此,内存和数据丢失问题久都解决了。但这样实现太繁琐了,一条数据被转移来转移去。

3、改进思路二:RabbitMQ

中间垫一个RabbitMQ,生产者和消费者都是文章审核服务自己。

在这里插入图片描述
好处:

  • 一来,存到RabbitMQ的数据,算服务器内存
  • 二来,RabbitMQ有自己的持久化机制,可靠

页面创建个交换机:
在这里插入图片描述
页面创建队列并绑定到交换机:(正常写生产消费者服务的代码里,这里做测试,直接页面创建)
在这里插入图片描述

调审核接口,往队列中存,这里存对象,拿Jackson框架的ObjectMapper转json,写到队列就返回给用户提交成功的信息:

@Autowired
private RabbitTemplate rabbitTemplate;
@Autowired
private ObjectMapper objectMapper;

@PostMapping("/demo3/{id}")
public void article3(@PathVariable("id") long id, @RequestBody ArticleDto article) throws JsonProcessingException {
    article.setId(id);
    rabbitTemplate.convertAndSend("jvm-test",null,  objectMapper.writeValueAsString(article));
}

监听队列,调阿里安全审核接口消费:

@Component
public class SpringRabbitListener {

    @RabbitListener(queues = "queue1",concurrency = "10")  //监听queue1队列,concurrency = "10"即启动十个线程并行处理
    public void listenSimpleQueue(String msg) throws InterruptedException {
        //消费
        System.out.println(msg);
        //用延迟模拟远程调用阿里云审核接口
        Thread.sleep(30 * 1000);
    }


}

启动同样配置的Jmeter脚本压测:

在这里插入图片描述

内存平稳,占用很低。(当然很低,占空间的都存MQ里了)
在这里插入图片描述

到此,优化完成。

4、总结

项目中要异步处理业务,或者实现生产者 – 消费者的模型,如果在Java代码中实现,那生产消费的速度、网络、远程调用的响应时间等影响,很有可能导致这些中间数据挤压,保存它们的同时占用了大量JVM堆内存,导致OOM,可使用MQ来实现。

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

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

相关文章

【运维】WSL1如何升级到WSL2

升级WSL1到WSL2&#xff1a;简便快捷版 在这篇博客中&#xff0c;我们将研究如何通过一种更简便的方式&#xff0c;将WSL1迅速升级到WSL2&#xff0c;避免官方文档的繁冗步骤。如果你觉得官方方法太过冗长&#xff0c;那么这里提供的步骤可能更适合你。 官网的办法是&#xf…

Cloudflare cdn 基本使用

个人版免费试用&#xff0c;一个邮箱账号只能缓存一个网站cdn。 地址&#xff1a;cloudflare.com 创建站点 在网站创建站点&#xff0c;填上你的域名 点击进入网站 缓存全局配置 可清除缓存&#xff0c;设置浏览器缓存时间 我设置了always online,防止服务器经常不稳定 缓…

Git学习笔记(第1章):Git概述

Git是一个免费的、开源的分布式版本控制系统&#xff0c;可以快速高效地处理从小型到大型的各种项目。 Git易于学习&#xff0c;占地面积小&#xff0c;性能极快。它具有廉价的本地库&#xff0c;方便的暂存区域和多个工作流分支等特性。其性能优于Subversion、CVS、Perforce 和…

Docker本地私有仓库搭建配置指导

一、说明 因内网主机需要拉取镜像进行Docker应用&#xff0c;因此需要一台带外主机作为内网私有仓库来提供内外其他docker业务主机使用。参考架构如下&#xff1a; 相关资源&#xff1a;加密、Distribution registry、Create and Configure Docker Registry、Registry部署、D…

Ivanti Connect Secure 曝两大零日漏洞,已被大规模利用

威胁情报公司Volexity发现&#xff0c;影响 Ivanti 的 Connect Secure VPN 和 Policy Secure 网络访问控制 (NAC) 设备的两个零日漏洞正在被大规模利用。自1月11日开始&#xff0c;多个威胁组织在大范围攻击中利用CVE-2023-46805身份验证绕过和CVE-2024-21887命令注入漏洞。 V…

Joern环境的安装(Windows版)

Joern环境的安装(Windows版) 网上很少有关于Windows下安装Joern的教程&#xff0c;而我最初使用也是装在Ubuntu虚拟机中&#xff0c;这样使用很占内存&#xff0c;影响体验感。在Windows下使用源码安装Joern也是非常简单的过程&#xff1a; 提前需要的本地环境&#xff1a; …

YOLOv5全网独家首发:DCNv4更快收敛、更高速度、更高性能,效果秒杀DCNv3、DCNv2等 ,助力检测实现暴力涨点

💡💡💡本文独家改进:DCNv4更快收敛、更高速度、更高性能,完美和YOLOv5结合,助力涨点 DCNv4优势:(1) 去除空间聚合中的softmax归一化,以增强其动态性和表达能力;(2) 优化存储器访问以最小化冗余操作以加速。这些改进显著加快了收敛速度,并大幅提高了处理速度,DCN…

vue:处理base64格式文件pdf、图片预览

一、需求&#xff1a;后端返回是base64数据&#xff0c;需要前端处理来展示文件。 二、实现方法&#xff1a; 解释一下这段代码的功能&#xff1a; &#xff09;preview(item) 是一个函数&#xff0c;接受一个参数 item&#xff0c;其中包含了文件的相关信息。 &#xff09;首…

SpringBoot的自定义starter和SpringBoot Starter机制,以及综合案例和通用模块-短信发送,基于AOP技术实现日志切面

目录 1.SpringBoot Starter机制 1.1.什么是SpringBoot Starter 1.2.为什么要使用SpringBoot Starter 1.3.应用场景 1.4.自动加载核心注解说明 2.综合案例 2.1.命名规范 2.2.通用模块-短信发送 2.2.1.创建配置类Properties 2.2.2.编写短信业务功能 2.2.3.创建自动配置…

基于Python+django影片数据爬取与数据分析设计与实现

目录 一、 前言介绍&#xff1a; 二 、功能设计&#xff1a; 三、功能实现&#xff1a; 系统登录实现 管理员实现 用户模块实现 四、库表设计&#xff1a; 五、关键代码&#xff1a; 六、论文参考&#xff1a; 七、其他案例&#xff1a; 八、源码获取&#xff1a; 一…

各省快递量数据, shp+excel,2001-2021年,已实现数据可视化

基本信息. 数据名称: 各省快递量数据 数据格式: shpexcel 数据时间&#xff1a;2001-2021年 数据几何类型: 面 数据坐标系: WGS84 数据来源&#xff1a;网络公开数据 数据字段&#xff1a; 序号字段名称字段说明1a_2001快递量/万件_2001年2a_2002快递量/万件_2002年3…

FairyGUI Day 1 导入FairyGUI

FairyGUI Unity3d引擎版本&#xff1a;Uinty3d 20233.2.3f1 1、从资产商店中将FairyGUI购入我的资产中&#xff0c;目前是免费的。 2、从我的资产中将FairyGUI导入到当前项目中。 3、我遇到的问题&#xff0c;我的Assets下有两个文件夹分别是Resources和Scenes&#xff0c;导…

AEB滤镜再破碎,安全焦虑「解不开」?

不久前&#xff0c;理想L7重大交通事故&#xff0c;再次引发了公众对AEB的热议。 根据理想汽车公布的事故视频显示&#xff0c;碰撞发生前3秒&#xff0c;车速在178km/h时驾驶员采取了制动措施&#xff0c;但车速大幅超出AEB&#xff08;自动紧急刹车系统&#xff09;的工作范…

【笔记】认识电机

认识电机 电机一些概念永磁同步电机电机的效率转矩永磁体定子和转子励磁电磁感应定律 AC Optimal Power Flow功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居…

esp32-idf eclipse 定时器的使用demo

esp32定时器的使用demo 1、介绍 ESP32芯片包含两个硬件定时器组。每组有两个通用硬件定时器。它们都是基于16位预分频器和64位自动重载功能的向上向下计数器的64位通用定时器。 2、API接口函数 创建定时器函数&#xff1a; esp_timer_create(); esp_err_t esp_timer_create …

P4学习(四)实验一:Basic Forwarding

目录 一.前置知识二.实验过程记录1.找到实验文件2.拓扑图3.明确实验内容4.实验初体验 三. 编写解决方案1.Parse部分1.1 Code1.2 知识点解析 2.Ingress部分2.1 Code2.2 知识点解析 3.Deparse部分3.1 Code3.2 知识点 四.实验完成测试 一.前置知识 Linux基础命令(vim)V!Model的架…

大模型多卡训练原理

背景知识 深度学习涉及大量矩阵运算&#xff0c;而矩阵运算可以并行计算。 一、数据并行 每张卡加载不同的数据&#xff0c;将计算结果合并 存在问题&#xff1a;每个显卡都加载了模型&#xff0c;浪费了一定空间 二、模型并行&#xff1a;适合模型特别大的情况 1、串行计算…

Java Springboot SSE如何判断客户端能否正常接收消息

目录 背景解决方案思路代码代码解释 Java反射知识点补充 背景 当新建一个 emitter 对象的时候, 它的默认超时时间是 30s. SseEmitter emitter new SseEmitter(); 但是很多情况下, 默认30s的时间太短, 需要把 emitter 对象的超时时间设置成不超时, 也就是永久有效. private …

RHEL8 Samba服务器详细配置用户模式

任务&#xff1a; 配置server01为samba服务器&#xff0c;samba服务器的/companydata/sales为共享目录&#xff0c;共享名为sales&#xff0c;里面创建测试文件test_share.tar&#xff0c;创建用户组sales&#xff0c;创建组内用户sale1&#xff0c;要求配置用户模式访问&#…

py爬虫入门笔记(request.get的使用)

文章目录 Day11. 了解浏览器开发者工具2. Get请求http://baidu.com3. Post请求https://fanyi.baidu.com/sug4. 肯德基小作业 Day21. 正则表达式2. 使用re模块3. 爬取豆瓣电影Top250的第一页4. 爬取豆瓣电影Top250所有的250部电影信息 Day31. xpath的使用2. 认识下载照片线程池的…