批量生成大量附件(如:excel,txt,pdf)压缩包等文件时前端超时,采用mq+redis异步处理和多线程优化提升性能

一.首先分析一下场景:项目中我需要从财务模块去取单证模块的数据来生成一个个excel文件
在单证那个一个提单号就是一个excel文件,我们这边一个财务发票可能会查出几千个提单,也就是会生成几百个excel,然后压缩为一个压缩包,这个时候在前端的话肯定是会超时,从而导致无法下载附件压缩包。
二.解决方案:mq+Redis+多线程异步处理
我们废话不多说,直接上代码思路,代码有些是封装的,所以可能大家不一定能用,大家在流的处理和压缩上可以用自己熟悉的,我们主要讲这个优化的过程和思路。poi和Redis和mq的大家自己选着用就行,poi我的4.1.2版本。
三.分案分为三大步:
1.创建批次号,将这个下载的参数和状态存入Redis中,然后用mq异步调用下载方法,返回批次号给到前端
2.mq消费消息进行文件下载本地或服务器进行保存
3.前端设置一个监听器触发器和监听处理器,去拿到这个第一步返回的批次号进行状态查询,这里的查询时到Redis中去查询,因为状态会存在Redis中,如果已经下载完成,会返回这个状态true,这个时候我们再去调用第三个接口,下载附件并压缩返回给浏览器

多线程的异步处理优化可以加在第二步,对附件进行生成并保存的时候。

四、具体实现代码如下(仅供参考):
1.首先你得创建一个存放批次号的类

@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class FinAutoDownloadParamDTO implements Serializable {/*** 批次号*/private String batchNo;/***后续用于查询数据用的参数*/private List<Long> invoiceIds;
}

2.这里是第一步的方法,用雪花算法创建出一个唯一的批次号,然后作为Redis的key,将下载的信息状态存入其中,将paramsDto插入mq调用的方法中,这个Redis大家可以spring的或者引入的Redis依赖,注入对象get()和set()就行

  public FinAutoDownloadFrResultVo exportFInToBookingExcelMQ(List<FinInvoiceReceiptVo> finInvoiceReceipts) {List<Long> invoiceIds = finInvoiceReceipts.stream().map(FinInvoiceReceiptVo::getFinInvoiceReceiptId).collect(Collectors.toList());if (CollectionUtils.isEmpty(invoiceIds)) {throw LocalizedExceptions.illegalArgument("Exception.data-no-select");}// 批次号,需保证该批次业务唯一String batchNo = snowFlakeGenerator.next().toString();String redisKey = FinConstant.FR_DOWN_PREFIX + batchNo;//校验批次号状态,如若正在计费则抛出异常Cache cache = FreightUtils.getCache();FinAutoDownloadParamDTO paramsDto = FinAutoDownloadParamDTO.builder().batchNo(batchNo).invoiceIds(invoiceIds).build();// 插入下载状态cache.put(redisKey, FinAutoDownloadStatus.builder().batchNo(batchNo).params(paramsDto).status(FinConstant.FinDownLoadStatus.PENDING).build());log.info("准备进行mq的舱单导出");finMqProducer.finManifestattachmentDown(paramsDto);log.info("财务舱单附件下载触发,参数:{}", com.gillion.ec.core.utils.JsonMapperHolder.jsonMapper.toJson(paramsDto));return FinAutoDownloadFrResultVo.builder().batchNo(batchNo).calcing(true).build();}

3.下面是mq来消费消息,获取Redis中的对象,来判断是否需要进行下载,下载过程创建线程池通过多线去下载,提高系统的响应速度,最后保存到你本地文件夹或者远程服务器

public void exportFInToBookingExcelMQDown(FinAutoDownloadParamDTO paramsDto) {// redis锁,防止重复String batchNo = paramsDto.getBatchNo();Cache cache = FreightUtils.getCache();String redisKey = FinConstant.FR_DOWN_PREFIX + batchNo;//获取redis中的对象,判断是否进行下载FinAutoDownloadStatus downloadStatus = cache.get(redisKey);if (Objects.isNull(downloadStatus)) {downloadStatus = FinAutoDownloadStatus.builder().batchNo(batchNo).params(paramsDto).status(FinConstant.FinDownLoadStatus.PENDING).build();}if (FinConstant.FinDownLoadStatus.RUNNING.equals(downloadStatus.getStatus())) {log.info("该业务批次正在下载,不能重复下载:{}", batchNo);return;}//这里我是获取业务数据进行后续附件的构造,你们按自己的需求去获取自己的数据就行//获取明细数据 拿到船名航次+提单号List<FinFreightItemR> execute = QFinFreightItemR.finFreightItemR.select().where(QFinFreightItemR.xsInvoiceId.in$(paramsDto.getInvoiceIds()).and(QFinFreightItemR.vesselNameEn.ne$(FinConstant.TOTAL_VESSELNAME))).limit(Integer.MAX_VALUE).execute();List<FinReceiveFreightFileVo> finFreightItems = CglibUtil.copyList(execute, FinReceiveFreightFileVo::new);Map<String, List<FinReceiveFreightFileVo>> finFreightsMap = finFreightItems.stream().collect(Collectors.groupingBy(item -> String.format("%s%s%s",item.getVesselNameEn(),item.getVoyageNo(),item.getSettlementCode())));List<VesselVoyageBlNoVo> vesselVoyageBlNoVoList =Lists.newArrayList();finFreightsMap.forEach((key,values)->{VesselVoyageBlNoVo vesselVoyageBlNoVo =new VesselVoyageBlNoVo();List<String> blNoList = finFreightItems.stream().map(FinReceiveFreightFileVo::getBlNo).distinct().collect(Collectors.toList());vesselVoyageBlNoVo.setBlNoList(blNoList);vesselVoyageBlNoVo.setOwnerCompany(values.get(0).getOwnerCompanyCode());vesselVoyageBlNoVo.setVesselCode(values.get(0).getVesselCode());vesselVoyageBlNoVo.setVoyageNo(values.get(0).getVoyageNo());vesselVoyageBlNoVo.setSettlementName(values.get(0).getSettlementName());vesselVoyageBlNoVoList.add(vesselVoyageBlNoVo);});//这个size很关键,是后续用于多线程等待的用的int size = vesselVoyageBlNoVoList.size();//创建CountDownLatch对象用于多线程计数final CountDownLatch latch =new CountDownLatch(size);String fileKey = null;String fileNameResult = null;String filePath = null;Long sysFileInfoId = null;Map<String, Object> resultMap = new HashMap<>();try {//压缩包名称String fileName = execute.get(0).getSettlementNameEn();String path = FileUtil.getTmpDirPath()  + File.separator +  UUID.randomUUID();String tempPath = path + File.separator +  fileName;//创建一级文件夹FileUtil.mkdir(tempPath);for (VesselVoyageBlNoVo vesselVoyageBlNoVo : vesselVoyageBlNoVoList) {//设置正在下载setRuningStatus(cache, redisKey, downloadStatus);//线程池获取线程异步分批进行下载threadPoolTaskExecutor.execute(()->{List<DocBookingHeadToFinVo> docBookingHeadToFinVos = docBookingHeadInterface.queryBookingHeadByFin(Collections.singletonList(vesselVoyageBlNoVo));log.info("财务舱单导出查询结果集docBookingHeadToFinVos大小:{}",docBookingHeadToFinVos.size());log.info("财务舱单导出查询结果集docBookingHeadToFinVos:{}",JsonMapperHolder.jsonMapper.toJson(docBookingHeadToFinVos));if(CollectionHelper.isNotEmpty(docBookingHeadToFinVos)){Map<String, List<DocBookingHeadToFinVo>> docBookingHeadToFinVosMap = docBookingHeadToFinVos.stream().collect(Collectors.groupingBy(item -> String.format("%s%s%s", item.getVesselNameEn(), item.getVoyageNo(), item.getManifestOwner())));log.info("财务舱单导出查询结果集docBookingHeadToFinVosMap大小:{}",docBookingHeadToFinVosMap.size());docBookingHeadToFinVosMap.forEach((key,values)->{//二级附件文件夹String tempPathForSecAttch = tempPath + File.separator +  key;FileUtil.mkdir(tempPathForSecAttch);Map<String, List<DocBookingHeadToFinVo>> docBookingMap = values.stream().collect(Collectors.groupingBy(DocBookingHeadToFinVo::getPol));for (Map.Entry<String, List<DocBookingHeadToFinVo>> entry : docBookingMap.entrySet()) {List<DocBookingHeadToFinVo> value = entry.getValue();try {exportCommExcel(value, tempPathForSecAttch,null, null);} catch (IOException e) {e.printStackTrace();}}});}//计数器减一latch.countDown();});}//线程等待,等待所有的异步线程都执行完后,才继续进行下一步latch.await();//压缩文件为zip tempath为我的一级目录File zipFile = ZipUtil.zip(tempPath);//将文件和路径存放于map中resultMap = getResultMap(zipFile, path);if(!resultMap.containsKey(EXPORT_FILE)){log.info("文件不存在:批次号{}", batchNo);return;}File zipFile2 = (File)resultMap.get(EXPORT_FILE);if(!FileUtil.exist(zipFile2)){log.info("文件导出失败:批次号{}", batchNo);return;}fileNameResult = zipFile.getName();filePath = zipFile.getAbsolutePath();//这里我们项目是将文件资源的byte流存远程,但是文件名和下载的关键key是放在数据库表中的,所有我这里会保存进去MultipartFile file = new MockMultipartFile(fileNameResult, fileNameResult, "", FileUtil.readBytes(zipFile));SysFileInfoDTO sysFileInfoDTO = sysFileInfoInterface.uploadFileForParam(FinConstant.ExcelUploadParam.UPLOAD_STRATEGY_ID,"Manifest_attachment_CW",Long.valueOf(paramsDto.getBatchNo()), file);fileKey = sysFileInfoDTO.getFileKey();sysFileInfoId = sysFileInfoDTO.getSysFileInfoId();}catch (Exception e) {log.error("文件下载失败:{}", e);} finally {//这里的fileKey,fileNameResult,sysFileInfoId就是我最后一步下载附件要用到的downloadStatus.setFileKey(fileKey);downloadStatus.setFileName(fileNameResult);downloadStatus.setSysFileInfoId(sysFileInfoId);setFinishStatus(cache, redisKey, downloadStatus);//我这里是建立的临时文件夹所有会把它删除掉FileUtil.del(filePath);if(resultMap.containsKey(EXPORT_FILE_TEMP_PATH)){String tempPath = (String)resultMap.get(EXPORT_FILE_TEMP_PATH);FileUtil.del(tempPath);}}}   

5.设置下载的状态


```java
//正在下载
private void setRuningStatus(Cache cache, String redisKey, FinAutoDownloadStatus downloadStatus) {downloadStatus.setStatus(FinConstant.FinDownLoadStatus.RUNNING);cache.put(redisKey, downloadStatus);}
//下载完成private void setFinishStatus(Cache cache, String redisKey, FinAutoDownloadStatus downloadStatus) {downloadStatus.setStatus(FinConstant.FinDownLoadStatus.FINISH);cache.put(redisKey, downloadStatus);}//将文件和路径存放于map中private Map<String, Object> getResultMap(File zipFile, String path) {Map<String,Object> resultMap = Maps.newHashMap();resultMap.put(EXPORT_FILE,zipFile);resultMap.put(EXPORT_FILE_TEMP_PATH,path);return resultMap;}   
5.查询是否附件以及全部生成并保存,没下载完FinReportDownoadVo 对象的FinishFlag字段值为false,给到前端去判断,然后继续调用查询,如果是true,则调用最后的下载方法```javapublic FinReportDownoadVo queryDownFrStatus(String batchNo) {FinReportDownoadVo frReportDownoadVo = new FinReportDownoadVo();if (StringUtils.isEmpty(batchNo)) {throw LocalizedExceptions.illegalArgument("Exception.fin.auto-freight.batch-no-is-empty");}String redisKey = FinConstant.FR_DOWN_PREFIX + batchNo;Cache cache = FreightUtils.getCache();FinAutoDownloadStatus status = cache.get(redisKey);if (Objects.isNull(status)) {throw LocalizedExceptions.illegalArgument("Exception.fin.down.batch-no-unmatch", batchNo);}log.info("FR 报表下载查询状态key={}状态为{}", batchNo, status.getStatus());if (!FinConstant.FinDownLoadStatus.FINISH.equals(status.getStatus())) {// 如若为空,则认定为MQ暂未消费// 如若不为空且状态不为完成,则认定为仍在消费中frReportDownoadVo.setFinishFlag(false);return frReportDownoadVo;} else {cache.del(redisKey);}frReportDownoadVo.setFinishFlag(true);frReportDownoadVo.setFileKey(status.getFileKey());frReportDownoadVo.setFileName(status.getFileName());frReportDownoadVo.setSysFileInfoId(status.getSysFileInfoId());return frReportDownoadVo;}

6.我这里前面说了下载资源已经保存到远程服务器,所以在查询状态的那步成功后会拿到这个filekey,我就你去远程下载这个压缩包的资源,在本地的在下载完那步不要删除,然后传文件的路径,通过IO流去本地获取是一样的。最后返回给页面就好了

 public void downloadFile(FinReportDownoadVo downloadParam, HttpServletRequest request, HttpServletResponse response) {if(StrUtil.isBlank(downloadParam.getFileKey())){throw LocalizedExceptions.illegalArgument("Exception.fin.down-report.file-not-exist");}ResponseEntity<byte[]> downFile = sysFileInfoInterface.downloadFileByKey(downloadParam.getFileKey());if(Objects.isNull(downFile) || Objects.isNull(downFile.getBody())){throw LocalizedExceptions.illegalArgument("Exception.fin.down-report.file-not-exist");}log.info("舱单附件下载filename:{}",downloadParam.getFileName());try {Servlets.setFileDownloadHeader(request, response,downloadParam.getFileName());IOUtils.write(downFile.getBody(), response.getOutputStream());} catch (IOException e) {throw new RuntimeException(e);}finally {sysFileInfoInterface.deleteFile(downloadParam.getSysFileInfoId());}}

看看执行效果图吧:
在这里插入图片描述

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

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

相关文章

【工具】2024年最新贵州省专业技术人员继续教育自动倍速刷课时脚本 - 篡改猴脚本

&#x1f525;&#x1f525;2024年最新贵州省专业技术人员继续教育自动倍速刷课时脚本|静音播放|自动跳过已完成的视频|解除防挂机提示|稳定极高 篡改猴脚本库国内地址&#xff1a;https://greasyfork.org/zh-CN/scripts/494638 一、自动播放脚本 脚本如下&#xff0c;仅供…

Zotero 使用入门(笔记)

参考文献&#xff1a;Zotero入门完整教程-共27节-免费&#xff0c;李长太>&#xff0c; 仅供参考学习

即插即用篇 | YOLOv8 引入 Strip Pooling | 重新思考场景解析的空间池化

本改进已集成到 YOLOv8-Magic 框架。 空间池化已被证明在捕获像素级预测任务的长距离上下文信息方面非常有效,如场景解析。在本文中,我们超越了通常具有N N规则形状的常规空间池化,重新思考空间池化的构成,引入了一种新的池化策略,称为条带池化,它考虑了一个长而窄的核,…

Redis不同数据类型value存储

一、Strings redis中String的底层没有用c的char来实现&#xff0c;而是使用SDS数据结构( char buf[])。 缺点:浪费空间 优势: 1.c字符串不记录自身的长度&#xff0c;所以获取一个字符串长度的复杂度是O(N),但是SDS记录分配的长度alloc,已使用长度len&#xff0c;获取长度的…

品味Fendi club:精酿啤酒的精致与与众不同

啤酒&#xff0c;作为世界三大饮料之一&#xff0c;其口感和品质的差异能给人们带来截然不同的体验。在众多啤酒中&#xff0c;Fendi club以其与众不同的精酿啤酒风格&#xff0c;吸引了无数热爱啤酒的人。 Fendi club啤酒的精致与与众不同&#xff0c;首先体现在其酿造工艺上。…

Nature子刊:常见口服药的副作用原来这么大!

哥伦比亚大学Harris H. Wang团队 在《Nature Microbiology》期刊上(IF28.3)发表了关于409种细菌-药物对揭示肠道微生物群扰动的驱动因素的文章&#xff0c;该研究通过对转录组学测定结果进行生信分析&#xff0c;强调了大规模转录组学在肠道微生物-外源化学物相互作用的功能发…

AI应用案例:供应链平台健康状况和发展趋势分析

某供应链平台在2019年就遍布了中国320个城市&#xff0c;为2600多家企业提供超40万个品类的供应链服务。它是通过直供城市终端销售门店&#xff0c;甚至是消费者&#xff0c;最大限度保证品牌和终端的销售利益。 但是平台交易市值较大、涉及的行业较多&#xff0c;而且打破了传…

6.数据库

1.实体用矩形表示&#xff0c;属性用椭圆表示&#xff0c;联系用菱形表示 2.层次模型用数表示 3.网状模型用图结构表示 4.关系模型用二维表格结构来表示 5.概念模式基本表 外模式视图 内模式存储 6.模式/内模式映像 外模式/模式映像 7.数据的物理独立性 跟内模式关系 逻辑是视图…

邦注科技给您解答 什么是注塑机模具保护器

模具监视器&#xff0c;这位制造业的守护神&#xff0c;时刻注视着模具的每一个细微变化。它的工作原理如同一位细心的侦探&#xff0c;利用传感器、数据采集系统和监控软件组成的精良装备&#xff0c;探寻模具的秘密。 传感器如同模具的耳目&#xff0c;敏锐地捕捉着模具的温度…

Github图片显示不出来?两步解决!

很多同学可能和我一样&#xff0c;在GitHub中找一些项目或者资料的时候&#xff1b;总是会看到一些图片显示不出来&#xff0c;或者数学公式乱码&#xff1a; 比如这样 还有这样 其实这个主要是因为DNS污染导致的&#xff0c;具体大家可以百度&#xff0c;这边不详细介绍。 解决…

LagentAgentLego智能体工具使用

1. lagent 参考文档 https://github.com/InternLM/Tutorial/blob/camp2/agent/lagent.md 使用 LMDeploy 部署 conda activate agent lmdeploy serve api_server /root/share/new_models/Shanghai_AI_Laboratory/internlm2-chat-7b \--server-name 127.0.0.1 \--model-name in…

JavaEE初阶-多线程4

文章目录 一、单例模式1.1 饿汉模式1.2 懒汉模式 二、阻塞队列1.1 生产者消费者模型1.1.1 现实生活举例1.1.2 生产者消费模型的两个优势1.1.2.1 解耦合1.1.2.2 削峰填谷 1.2 阻塞队列代码1.2.1 使用java标准库的阻塞队列实现生产者消费者模型1.2.2 实现自己的阻塞队列 一、单例…

30年赚1000亿美元--“量化之王”和他最传奇的基金“大奖章”的秘密

文艺复兴是华尔街最成功、最神秘的机构之一。从1988-2018年的30年里&#xff0c;文艺复兴仅向内部员工开放的旗舰基金“大奖章”累计创造了超过1000亿美元的收益&#xff0c;年均回报率高达39%。作为对比&#xff0c;同期“股神”巴菲特的年均回报率为20.5%。 而且&#xff0c;…

【Linux】-IP地址、主机名配置[5]

目录 一、IP和主机名 1、IP地址 2、特殊IP地址 3、主机名 4、在Linux中修改主机名 5、配置主机名映射 二、虚拟机配置固定IP 1、为什么需要固定IP 2、在VMware Workstation中配置固定ip 一、IP和主机名 1、IP地址 每一台联网的电脑都会有一个地址&#xff0c;用于和…

大模型面试常考知识点1

文章目录 1. 写出Multi-Head Attention2. Pre-Norm vs Post-Norm3. Layer NormRMS NormBatch Norm 4. SwiGLU从ReLU到SwishSwiGLU 5. AdamW6. 位置编码Transformer位置编码RoPEALibi 7. LoRA初始化 参考文献 1. 写出Multi-Head Attention import torch import torch.nn as nn …

QT6 android程序界面强制横屏显示不旋转

QT6开发的Android程序有时候旋转后程序会变形&#xff0c;比如想让其固定位横屏显示&#xff0c;就需要进行特殊设置&#xff0c;本文提供一种简便的设置方法。 一.AndroidManifest.xml文件介绍 Android的Manifest.xml文件是一个重要的配置文件&#xff0c;用于描述应用程序的…

2024最新从0部署Django项目(nginx+uwsgi+mysql)

云服务器 我这里用的是腾讯云免费试用的2H4Gcentos服务器&#xff08;后升级为2H8G&#xff0c;保险一点提高内存&#xff09; 因为网上很多关于django部属的教程都是宝塔啊&#xff0c;python版本控制器啊这种的&#xff0c;我也误打误撞安装了宝塔面板&#xff0c;但这里我…

浅谈运维数据安全

在数字化日益深入的今天&#xff0c;运维数据安全已经成为企业信息安全体系中的核心要素。运维工作涉及到企业信息系统的各个方面&#xff0c;从硬件维护到软件升级&#xff0c;从网络配置到数据备份&#xff0c;无一不需要严谨的数据安全保障措施。本文将从运维数据安全的重要…

民航电子数据库:select查询时部分字段缺失

目录 前言异常排查原因解决使用systemPath标签引入本地Jar包后无法打包 前言 1、对接民航电子数据库 2、框架为shardingsphere caedb mybatis 3、部分SQL查询时&#xff0c;会出现字段缺失的情况 4、查看日志打印出来的SQL&#xff0c;字段并未缺失 异常 这里省略SQL语句…

FreeRTOS事件组

什么是事件标志组? 事件标志位 :表明某个事件是否发生,联想:全局变量 flag 。通常按位表示,每一个位表示一个事件(高8 位不算) 事件标志组 是一组事件标志位的集合, 可以简单的理解事件标志组,就是一个整数。 事件标志组本质是一个 16 位或 32 位无符号的数据类型…