导出Excel数据量大时间过慢问题

导出Excel数据量大时间过慢问题

最近在工作中遇到一个问题,就是一个普通的导出Excel功能,响应时间居然需要一分多钟,导出的数据量为 10W-15W条

问题发现:

导出Excel无非就三个步骤
1: 查询数据
2: 数据处理
3: 导出数据
通过打日志的方式,发现查询和导出占用的时间很短,大量的时间都用来处理数据了。
查看代码发现,数据处理部分用的for循环处理,那就意味着需要遍历10W+遍,所以这个地方肯定是不可以这么写的

解决思路:

1: 结果集判断,如果结果集数据量较大,则将结果集进行分组
2: 分组后的N组数据 通过CountDownLatch用多线程去并行处理N组数据
3: 汇总N组数据,组装到一个List里
4: 调用导出工具类执行导出方法

代码:

导出Excel方法:

    @Log(value = "导出结算明细")
    @RequestMapping("/export_detail")
    public void exportDoctorSettleDetail(SjAdsFinanceDoctorSettleDetailQuery detailQuery, HttpServletResponse response) {
        try {
            log.info("[BIZ Normal] MSG exportDoctorSettleDetail params:{}", JsonUtil.toString(detailQuery));
            //根据筛选条件查询列表信息
            List<SjAdsFinanceDoctorSettleDetail> detailList = sjAdsFinanceDoctorSettleDetailService.findDetailListForExport(detailQuery, null);
            log.info("[BIZ Normal] MSG exportDoctorSettleDetail select end");
            if (CollectionUtils.isEmpty(detailList)) {
                log.error("[BIZ Exception] Exception_MSG Class exportDoctorSettleDetail not find doctorSettleDetail, query={}", detailQuery);
            }
            List<DrugstoreDoctorSettleDetailForListVO> allDataList = new ArrayList<>();
            //如果导出的数据量过大(目前是大于1000条),则将数据分组多线程进行处理
            if (detailList.size() > Constant.EXPORT_DATA_LIMIT) {
                final CountDownLatch latch = new CountDownLatch(Constant.GROUP_SIZE);
                // 调用方法将集合分隔为多个小集合
                List<List<SjAdsFinanceDoctorSettleDetail>> groupList = groupList(detailList, Constant.GROUP_SIZE);
                // 此处线程池最好不要每次都新new,用公用的线程池,此处省事我直接new了
                ThreadPoolExecutor service = new ThreadPoolExecutor(
                        Constant.GROUP_SIZE, Constant.GROUP_SIZE, 60, TimeUnit.SECONDS,
                        new LinkedBlockingQueue<Runnable>(),
                        new ThreadPoolExecutor.DiscardOldestPolicy());
                // 循环groupList,多线程处理每一个小集合数据        
                for (List<SjAdsFinanceDoctorSettleDetail> list : groupList) {
                    Runnable runnable = new Runnable() {
                        @Override
                        public void run() {
                        	try{
                        		// 此处处理小集合的数据,做啥都可以,做你需要处理的事情
	                            List<DrugstoreDoctorSettleDetailForListVO> doctorSettleDetailList = list.stream().map((t) -> {
	                                return exportDetailToVo(t);
	                            }).collect(Collectors.toList());
	                            //将处理好的数据追加到allDataList中
	                            allDataList.addAll(doctorSettleDetailList);
	                        }catch (RuntimeException e){
				                e.getMessage();
				            }finally {
				                //不管上面执行结果如何,当前线程调用此方法,计数减一
                            	latch.countDown();
				            }
                        }
                    };
                    // 嫌麻烦的可以改用lambda写法~
                    service.execute(runnable);
                }
                try {
                    //阻塞当前线程,直到计数器的值为0
                    //上方的多个线程全部处理完数据前,此处阻塞,知道所有线程都处理完数据
                    latch.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    service.shutdown();
                }
            } else {
                List<DrugstoreDoctorSettleDetailForListVO> doctorSettleDetailList = detailList.stream().map(this::exportDetailToVo).collect(Collectors.toList());
                allDataList.addAll(doctorSettleDetailList);
            }
            // 在这就可以拿到汇总的处理结果数据,如何做最后的数据导出动作了
            String fileName = detailQuery.getReportDate() + "_" + detailQuery.getReportDate() + "_结算明细_" + DateUtil.format(new Date(), DateUtil.PATTERN_YEAR2MINUTE);
            List<String> filedNames = handleManager.get(getCurrentUser().getId(), DrugstoreDoctorSettleDetailForListVO.class, allDataList);
            filedNames = filedNames.stream().filter(str -> !"doctorAccount".equals(str)).collect(Collectors.toList());
            log.info("[BIZ Normal] MSG exportDoctorSettleDetail result:{}", JsonUtil.toString(allDataList));
            EasyExcelUtil.exportExcel(response, DrugstoreDoctorSettleDetailForListVO.class, fileName).includeColumnFiledNames(filedNames).sheet(fileName).doWrite(allDataList);
            log.info("[BIZ Normal] MSG exportDoctorSettleDetail end");
        } catch (Exception e) {
            log.error("[BIZ Exception] Exception_MSG = {}", e.toString());
            throw new ServiceError("500","导出结算明细异常");
        }
    }

List分组方法

    /**
     * List分割操作
     *
     * @param sourceList 需要分割的List
     * @return 分割的list集合
     */
    public static List<List<SjAdsFinanceDoctorSettleDetail>> groupList(List<SjAdsFinanceDoctorSettleDetail> sourceList, Integer groupNum) {
        List<List<SjAdsFinanceDoctorSettleDetail>> groupList = new ArrayList<>();
        int total = sourceList.size();
        // 计算出余数
        int remainder = total % groupNum;
        // 计算出商
        int number = total / groupNum;
        // 偏移量
        int offset = 0;
        for (int i = 0; i < groupNum; i++) {
            List<SjAdsFinanceDoctorSettleDetail> subList;
            if (remainder > 0) {
                subList = sourceList.subList(i * number + offset, (i + 1) * number + offset + 1);
                remainder--;
                offset++;
            } else {
                subList = sourceList.subList(i * number + offset, (i + 1) * number + offset);
            }
            groupList.add(subList);
        }
        return groupList;
    }
### Java EasyExcel 导出大数据Excel 文件的最佳实践和性能优化 #### 1. 分页查询数据库 对于规模数据导出,一次性从数据库获取所有记录可能导致内存不足。采用分页查询可以有效降低单次查询返回的数据量,从而减轻内存压力。 ```java // 假设每页小为1000条记录 int pageSize = 1000; for (int pageNum = 1; ; pageNum++) { List<Data> dataList = dataService.findDataByPage(pageNum, pageSize); if (dataList.isEmpty()) break; // 将当前页的数据写入Excel文件 } ``` 此方法通过控制每次读取的数据量来提高程序稳定性[^3]。 #### 2. 利用EasyExcel流式写入功能 传统方式下,整个工作表会被加载至内存再写出,而EasyExcel支持按需读/写行级数据,极减少了资源消耗并提升了处理速度。 ```java String fileName = "large_data.xlsx"; try ( Workbook workbook = new SXSSFWorkbook(); // 创建SXSSFWorkBook对象用于创建临时文件 FileOutputStream fos = new FileOutputStream(fileName)) { Sheet sheet = workbook.createSheet(); // 设置批处理模式下的最行数,默认值为100 ((SXSSFSheet)sheet).setRandomAccessWindowSize(100); int rowIndex = 0; while(hasMoreData()){ Row row = sheet.createRow(rowIndex++); Cell cell = row.createCell(0); cell.setCellValue(getCellValue()); } workbook.write(fos); } catch(Exception e){ throw new RuntimeException(e.getMessage(),e.getCause()); } ``` 上述代码片段展示了如何利用`SXSSFWorkbook`类实现高效的规模数据写入操作[^4]。 #### 3. 合理规划Sheet结构 当面对海量数据时,应避免将过多的信息集中于单一表格内。可以通过拆分为多个Sheets或将部分内容独立成新的Workbook等方式来进行合理布局,既方便查看又利于提升整体性能表现。 #### 4. 减少不必要的样式设置 复杂的单元格格式化(如字体颜色、边框线型等)会在一定程度上拖慢生成过程。因此,在不影响业务逻辑的前提下尽可能简化外观设计有助于加快执行进度。 #### 5. 并发多线程处理 针对超型数据集可考虑引入并发机制加速任务完成时间。不过需要注意的是,并发度不宜过高以免造成其他方面的问题比如锁竞争或者网络瓶颈等问题的发生。
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Ellis_li

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值