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

news2024/11/17 1:42:02

一.首先分析一下场景:项目中我需要从财务模块去取单证模块的数据来生成一个个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,则调用最后的下载方法

```java
 public 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.coloradmin.cn/o/1665419.html

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

相关文章

接口自动化测试很难掌握吗?

一. 什么是接口测试 接口测试是一种软件测试方法&#xff0c;用于验证不同软件组件之间的通信接口是否按预期工作。在接口测试中&#xff0c;测试人员会发送请求并检查接收到的响应&#xff0c;以确保接口在不同场景下都能正常工作。 就工具而言&#xff0c;常见的测试工具有…

AI写作推荐-写文ai-AI在线写作生成器-3步完成写作任务

AI写作利器&#xff1a;推荐几款神助攻文案创作工具 随着技术的进步&#xff0c;人工智能&#xff08;AI&#xff09;已达到高级水平&#xff0c;在众多领域展现其强大能力。 在文本创作的领域&#xff0c;人工智能&#xff08;AI&#xff09;应用已显著地提升了写作效率和创意…

【计算机网络】物理层传输介质 习题3

双绞线是用两根绝缘导线绞合而成的&#xff0c;绞合的目的是( )。 A.减少干扰 B.提高传输速度 C.增大传输距离 D.增大抗拉强度 在电缆中采用屏蔽技术带来的好处主要是( ) A.减少信号衰减 B. 减少电磁干扰辐射 C.减少物理损坏 D. 减少电缆的阻抗 利用一根同轴电缆互连主机构成…

多功能投票小程序基于ThinkPHP+FastAdmin+Uniapp(源码搭建/上线/运营/售后/维护更新)

基于ThinkPHPFastAdminUniapp开发的多功能系统&#xff0c;支持图文投票、自定义选手报名内容、自定义主题色、礼物功能(高级授权)、弹幕功能(高级授权)、会员发布、支持数据库私有化部署&#xff0c;Uniapp提供全部无加密源码。 功能特性

Shopee虾皮行业分析:灯具类目市场价值超15亿,打造爆品先选好品

在东南亚这个充满活力的地区&#xff0c;灯具市场正如同其璀璨的夜空&#xff0c;闪烁着无限的可能性。 从繁华的新加坡到古老的曼谷&#xff0c;从繁忙的雅加达到宁静的河内&#xff0c;灯具在每个角落都扮演着至关重要的角色。 它们不仅照亮了家庭的温馨空间&#xff0c;也…

【C语言】/*操作符(下)*/

目录 一、操作符的分类 二、二进制和进制转换 2.1 进制 2.2 进制之间的转换 三、原码、反码、补码 四、单目操作符 五、逗号表达式 六、下标引用操作符[] 七、函数调用操作符() 八、结构体成员访问操作符 8.1 直接访问操作符(.) 8.2 间接访问操作符(->) 九、操作符…

【智能算法应用】基于果蝇算法-BP回归预测(FOA-BP)

目录 1.算法原理2.数学模型3.结果展示4.代码获取 1.算法原理 【智能算法应用】智能算法优化BP神经网络思路【智能算法】果蝇算法&#xff08;FOA&#xff09;原理及实现 2.数学模型 数据集样本特征数为13&#xff0c;适应度函数设计为&#xff1a; f i t n e s s e r r o…

skywalking的使用

文章目录 介绍概念介绍探针agent后台服务 使用后台界面查询异常接口查看访问量 遇到的问题 介绍 官网 https://skywalking.apache.org/ 安装包下载 https://skyapm.github.io/document-cn-translation-of-skywalking/ 组成 Agent&#xff08;探针&#xff09;&#xff1a;Ag…

UIKit之UIButton

功能需求&#xff1a; 点击按钮切换按钮的文字和背景图片&#xff0c;同时点击上下左右可以移动图片位置&#xff0c;点击加或减可以放大或缩小图片。 分析&#xff1a; 实现一个UIView的子类即可&#xff0c;该子类包含多个按钮。 实现步骤&#xff1a; 使用OC语言&#xf…

使用pandas的merge()和join()函数进行数据处理

目录 一、引言 二、pandas的merge()函数 基本用法 实战案例 三、pandas的join()函数 基本用法 实战案例 四、merge()与join()的比较与选择 使用场景&#xff1a; 灵活性&#xff1a; 选择建议&#xff1a; 五、进阶案例与代码 六、总结 一、引言 在数据分析和处理…

stata空间计量模型基础+检验命令LM检验、sem、门槛+arcgis画图

目录 怎么安装stata命令 3怎么使用已有的数据 4数据编辑器中查看数据 4怎么删除不要的列 4直接将字符型变量转化为数值型的命令 4改变字符长度 4描述分析 4取对数 5相关性分析 5单位根检验 5权重矩阵标准化 6计算泰尔指数 6做核密度图 7Moran’s I 指数 8空间计量模型 9LM检验…

Java | Leetcode Java题解之第68题文本左右对齐

题目&#xff1a; 题解&#xff1a; class Solution {private String line(List<String> list,int maxWidth,int totalLength,boolean isLast){StringBuilder sb new StringBuilder();sb.append(list.get(0));if(list.size() 1){String ap " ".repeat(maxW…

interview_bak

flink内存管理 JVM 存在的几个问题: Java 对象存储密度低。一个只包含 boolean 属性的对象占用了16个字节内存:对象头占了8个,boolean 属性占了1个,对齐填充占了7个。而实际上只需要一个bit(1/8字节)就够了。Full GC 会极大地影响性能,尤其是为了处理更大数据而开了很大…

Nest.js中使用任务调度

java中的xxl在nestJs中是有内置的任务调度nestjs/schedule npm install --save nestjs/schedule 在model中引入使用 在service中直接使用就行 具体间隔多久看官方配置 Task Scheduling | NestJS 中文文档 | NestJS 中文网

DDoS攻防,本质上是成本博弈!

在互联网里&#xff0c;分布式拒绝服务&#xff08;DDoS&#xff09;攻击作为一种常见的网络威胁&#xff0c;持续对网站、在线服务和企业基础设施构成严重挑战。本文旨在探讨实施DDoS攻击的大致成本、以及企业如何采取有效措施来防范此类攻击&#xff0c;确保业务连续性和网络…

二叉树进阶 --- 中

目录 1. find 的递归实现 2. insert 的递归实现 3. erase 的递归实现 3.1. 被删除的节点右孩子为空 3.2. 被删除的节点左孩子为空 3.3. 被删除的节点左右孩子都不为空 4. 析构函数的实现 5. copy constructor的实现 6. 赋值运算符重载 7. 搜索二叉树的完整实现 1. fi…

IM 是什么?

在当今数字化的时代&#xff0c;即时通讯&#xff08;IM&#xff09;已经渗透到人们的日常生活和企业的工作环境中。IM技术的快速i发展为人们提供了一种高效、便捷的沟通方式&#xff0c;不仅推动了社会的信息化进程&#xff0c;也提升了企业的协同效率和竞争力。 作为企业级I…

API接口调用|京东API接口|淘宝API接口

什么是电商API接口&#xff1a; 电商API接口是电商服务平台对外提供的一种接口服务&#xff0c;允许第三方开发者通过编程方式与电商系统进行数据交互和功能调用。 这些接口提供了一种标准化的方法来获取、更新或处理电商平台上的商品信息、订单状态、用户数据、支付信息、物流…

堆排序 之实现最小的K个数

目录 1、方式一&#xff1a;通过自定义实现建堆和堆化操作 2、方式二&#xff1a;借助模块heapq实现 2.1、模块heapq的基本使用 2.2、使用heapq实现最小的k个数 3、堆在实际项目的应用 实现语言&#xff1a;Python 3.9 题目来源&#xff1a;牛客 分析&#xff1a; 要找…

Offline: Overcoming Model Bias for Robust Offline Deep Reinforcement Learning

EAAI 2023 paper Intro model-free的离线强化学习由于价值函数估计问题存在训练的稳定性以及鲁棒性较低。本文提出基于模型的方法&#xff0c;同构构建稳定的动力学模型帮助策略的稳定训练。 method 本文基于模型的方法&#xff0c;所构造的转移模型输入状态动作&#xff0…