批量生成大量附件(如: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,则调用最后的下载方法

```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());
        }
    }

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

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

企业微信创建应用(一)

登录到企业微信后台管理(https://work.weixin.qq.com/)进入自建应用(应用管理-应用-创建应用) 3.查看参数AgentId和 Secret 4.企业微信查看效果

Python | Leetcode Python题解之第84题柱状图中最大的矩形

题目&#xff1a; 题解&#xff1a; class Solution:def largestRectangleArea(self, heights: List[int]) -> int:n len(heights)left, right [0] * n, [n] * nmono_stack list()for i in range(n):while mono_stack and heights[mono_stack[-1]] > heights[i]:righ…

音频运放LPF使用

1、用于差分ADC输入的单端到差分缓冲器和抗混叠LPF 2、用于单端ADC输入的缓冲和抗混叠LPF 3、用于DAC输出的后置LPF和差分到单端缓冲器&#xff08;AC耦合&#xff09; 4、后LPF和差分到DAC输出的单端缓冲器&#xff08;直流耦合&#xff09; 5、用于差分ADC输入的具有抗混叠LP…

【无标获取S4与ECC的具体差异的方法题】

首先我们需要对ECC vs S4的差异这个课题要有一个深刻的理解&#xff0c;这不是一个简单并能准确说清楚的课题。 我们需要结合实际项目的具体情况去回答这个问题&#xff0c;因为这个问题本身是没有标准答案的。 首先要了解SAP本身ERP产品线的发展概况&#xff0c;其次我们要…

微信小程序踩坑,skyline模式下,scroll-view下面的一级元素设置margin中的auto无效,具体数据有效

开发工具版本 基础库 开启skyline渲染调试 问题描述 skyline模式下,scroll-view下面的一级元素的margin写auto的值是没有效果的(二级元素margin写auto是有效果的),关闭这个模式就正常显示 演示效果图 父元素的宽度和高度效果(宽度是750rpx,宽度占满的) 一级元素宽度和css效果…

在Linux上安装并启动Redis

目录 安装gcc环境 上传redis文件方法一&#xff1a;sftp 上传redis文件方法二&#xff1a;wget 启动redis-server ctrlc关闭redis-server 参考文章&#xff1a;Linux 安装 Redis 及踩坑 - 敲代码的阿磊 - 博客园 (cnblogs.com) 准备&#xff1a;打开VMware Workstation&am…

20240511,谓词,内建函数对象

拜托铠甲勇士真的帅好不好&#xff01;&#xff01;&#xff01; STL案例2-员工分组 10个员工&#xff0c;指派部门&#xff0c;员工信息&#xff08;姓名&#xff0c;工资组成&#xff0c;部门&#xff1a;策划&#xff0c;美术&#xff0c;研发&#xff09;&#xff0c;随机…

【Web】2023香山杯决赛 security system 题解

目录 step -1 step 0 step 1 step 2 step 3 step -1 ①题目hint&#xff1a;想办法修改属性值后进入java的原生反序列化&#xff0c;然后利用jackson链写入内存马 ②jackson反序列化基础&#xff1a; ObjectMapper objectMapper new ObjectMapper(); String jsonStrin…

MyBatis-plus(一):快速入门

目录 一、MyBatis-plus 快速入门 1、原理 2、实体类命名规则 3、常见注解 4、主键 id 策略 5、使用 TableField 的常见场景 6、常用配置 二、核心功能 1、条件构造器 2、自定义 SQL 3、IService 接口 一、MyBatis-plus 快速入门 1、原理 MyBatisPlus 通过扫描实体…

✨✨使用vue3打造一个el-form表单及高德地图的关联组件实例✨

✨1. 实现功能 &#x1f31f;表单内显示省市县以及详细地址 点击省市县输入框时&#xff0c;打开对应地图弹窗&#xff0c;进行位置选择选择位置回显入对应输入框表单内的省市县以及地址输入框同外嵌表单走相同的校验方式触发校验后点击reset实现清除校验与清空数据 &#x1f…

WebSocket 详解加入门实操理解加深

WebSocket 介绍 WebSocket 是基于 TCP 的一种新的网络协议。它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手&#xff0c;两者之间就可以创建持久性的连接&#xff0c; 并进行双向数据传输。 HTTP协议和WebSocket协议对比&#xff1a; HTTP是短连接…

图的顺序存储和链式存储实现

目录 一、顺序存储 有向图&#xff1a; 无向图 代码实现 二、链式存储 有向图 无向图 代码实现 一、顺序存储 主要用到的是一个二维数组&#xff0c;也就是矩阵&#xff0c;直接上栗子&#xff1a; 有向图&#xff1a; 若要储存如下这个有向图&#xff1a; 需要建立一…

前端小程序调用 getLocation 实现地图位置功能,通过 纬度:latitude 经度: longitude 获取当前位置

1、首先登录一下 腾讯的位置服务 有账号就登录没账号就注册&#xff0c; 点击右上角的控制台点击左侧的应用管理 ---> 我的应用 ---->> 创建应用 1、创建应用 2、列表就会显示我们刚刚创建好的 key 3、点击添加 key 4、按照要求填写信息 我们用的是小程序 所以选择…

Spring编程使用DDD的小把戏

场景 现在流行充血领域层&#xff0c;在原本只存储对象的java类中&#xff0c;增加一些方法去替代原本写在service层的crud&#xff0c; 但是例如service这种一般都是托管给spring的&#xff0c;我们使用的ORM也都托管给spring&#xff0c;这样方便在service层调用mybatis的m…

电影院购票管理系统

文章目录 电影院购票管理系统一、项目演示二、项目介绍三、部分功能截图四、部分代码展示五、底部获取项目源码&#xff08;9.9&#xffe5;带走&#xff09; 电影院购票管理系统 一、项目演示 电影院售票管理系统 二、项目介绍 基于springbootvue的前后端分离电影院购票管理…

Hive行列转换应用与实现

Hive行列转换应用与实现 1.多行转多列 问题引入 解决方法 2.多行转单列 问题引入 解决方法 3.多列转多行 问题引入 解决方法 4.单列转多行

部署yolov5

1 创建一个yolov5的环境 conda create -n yolov5 python3.8 2 激活环境 conda activate yolov5 3 设置清华源 pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple 4 PyTorch 网站下载pytorch 备注:也可以使用pip install 5 下载 yolov5…

Linux实验三:文件属性及目录操作

目录 一、实验目的二、实验内容三、实验环境四、参考代码五、实验步骤步骤1. 编辑源代码test3.c步骤2. 编译源代码test3.c步骤3. 运行可执行文件test3 六、实验结果七、实验总结 一、实验目的 1、理解Linux中的目录及i节点等基本概念&#xff1b; 2、掌握目录的读写时常用的函…

OpenHarmony 实战开发——轻量带屏解决方案之恒玄芯片移植案例

本文章基于恒玄科技BES2600W芯片的欧智通 Multi-modal V200Z-R开发板 &#xff0c;进行轻量带屏开发板的标准移植&#xff0c;开发了智能开关面板样例&#xff0c;同时实现了ace_engine_lite、arkui_ui_lite、aafwk_lite、appexecfwk_lite、HDF等部件基于OpenHarmony LiteOS-M内…

PCIE协议-2-事务层规范-Message Request Rules

2.2.8 消息请求规则 本文档定义了以下几组消息&#xff1a; INTx 中断信号电源管理错误信号锁定事务支持插槽电源限制支持厂商定义消息延迟容忍度报告&#xff08;LTR&#xff09;消息优化缓冲区冲洗/填充&#xff08;OBFF&#xff09;消息设备就绪状态&#xff08;DRS&#…