poi多线程导出

目录

前言

poi导出问题

优化背景

优化思路

代码实现


前言

以前就想记录下关于导出的东西,正好最近又遇到了数据量偏大,需要优化导出的一个项目,这里稍微记录下思路,以及一些垃圾代码~

poi导出问题

  1. 查询数据量过大,导致内存溢出或者时间耗时比较严重(通过多线程分批查询)
  2. 最后下载的时候excel转换的输出流内存溢出(可通过新版的SXSSFWorkbook解决,可通过其构造函数指定在内存中缓存的行数,剩余的会自动缓存在硬盘的临时目录上)

优化背景

项目中的导出实际上已经优化过一版了,原先就是最常见的poi导出方式,随着数据增大,上述1,2问题越来越明显,后来前人改过一版,通过缓存方式实现,即程序启动时异步导出所有数据放到服务器,当提交数据或修改数据时异步调整服务器上的文件,用户点击导出时直接从服务器上下载文件,可想而知速度还是很快的。

使用缓存简单方便,奈何该实现存在并发的情况,当该业务使用的人过多,会出现频繁修改该excel,而且每个线程修改时耗时也比较慢,导致线程阻塞,使得内存压力不断增大,最终内存溢出,程序直接gg

优化思路

  1. 当数据量很大或者查询逻辑很复杂导致最后的耗时一直很严重,那么可以调整导出的逻辑,后台提供三个接口,一个接口直接异步使用多线程进行导出,同时将导出进度存入redis,另外一个接口每3s循环调用查询导出进度,当进度为100时,直接下载即可
  2. 数据量不是特别大时,那么可以直接使用多线程进行excel读写导出

代码实现

代码贴的是第二种思路,写的很垃圾,权当记录,主旨就是通过多线程查询数据然后各自封装到poi中,其实也可以使用 fork-join ,以前写过一篇文章介绍:并发编程介绍

我们的场景是数据需要以月份进行分离,因此sheet页需要按月份进行排序,原先每个sheet页中的数据也需要按时间排序,后来与产品讨论为了节省时间去掉该逻辑,可在导出后通过excel自身的排序满足

    /**
     * 每个线程处理的普危货数量
     */
    private static final int THREAD_THRESHOLD = 300;
    /**
     * 计数器
     */
    private CountDownLatch countDownLatch;
    /**
     * sheet页锁对象
     */
    private static Map<Sheet, Object> lockMap = new ConcurrentHashMap<>();

    public void realTimeExport(HttpServletResponse response, Integer type) {
        long startTime = System.currentTimeMillis();
        List<String> monthList = orderPassApplyMapper.listMonthList(type);
        if (CollectionUtils.isEmpty(monthList)) {
            throw new AsocoSealingParkException("当前普危货无数据,无法导出");
        }
        Workbook workbook = null;
        try {
            // 首先创建sheet页,确保顺序
            workbook = new SXSSFWorkbook(100);
            CellStyle titleStyle = workbook.createCellStyle();
            titleStyle.setFillForegroundColor(IndexedColors.TAN.getIndex());
            titleStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);

            String sheetSuffix = type == 0 ? "危化无单" : "普货进园";
            for (String month : monthList) {
                Sheet sheet = workbook.createSheet(month + sheetSuffix);
                // 创建标题行
                Row row = sheet.createRow(0);
                String[] title = new String[] {"卸货企业名称", "装货企业名称", "运输意向", "申请时间", "牵引车车牌", "挂车车牌", "司机姓名", "司机身份证号", "司机联系方式", "装货企业联系人", "装货企业联系方式", "卸货企业联系人",
                        "卸货企业联系方式", "申请备注", "申请进园时间开始", "申请进园时间截止", "允许入园时间开始", "允许入园时间截止", "企业审核时间", "企业审核意见", "企业审核人", "企业审核状态"};
                for (int i =0; i< title.length; i++) {
                    sheet.setColumnWidth(i, (int) (35.7 * 200));
                    Cell cell0 = row.createCell(i);
                    cell0.setCellValue(title[i]);
                    cell0.setCellStyle(titleStyle);
                }
            }

            Wrapper<OrderPassApplyDo> wrapper = new EntityWrapper<>();
            wrapper.eq("type", type);
            int total = orderPassApplyMapper.selectCount(wrapper);

            // 确定线程数量
            int threadNums = total % THREAD_THRESHOLD == 0 ? (total / THREAD_THRESHOLD) : (total / THREAD_THRESHOLD) + 1;

            log.info("sheet页创建耗时:{}", (System.currentTimeMillis() - startTime));
            long time = System.currentTimeMillis();
            countDownLatch = new CountDownLatch(threadNums);

            for (int i = 0; i < threadNums; i++) {
                ThreadPoolManager.getInstance().getExecutorService().submit(new ExportTask(workbook, i * THREAD_THRESHOLD, sheetSuffix, type));
            }
            countDownLatch.await();
            log.info("多线程数据封装耗时:{}", (System.currentTimeMillis() - time));

            response.setHeader("content-Type", "application/vnd.ms-excel;charset=UTF-8");
            response.setCharacterEncoding("UTF-8");
            response.setHeader("Content-Disposition", "attachment;filename=" + ExcelUtil.processFileName(sheetSuffix + ".xlsx"));

            workbook.write(response.getOutputStream());

        } catch (Exception e) {
            log.error("导出普/危货数据出错,{}", e.getMessage());
        } finally {
            try {
                if (null != workbook) {
                    workbook.close();
                }
            } catch (Exception e) {
                log.error("关闭 workbook 失败,{}", e.getMessage());
            }
        }
        log.info("普危货导出总耗时:{}", (System.currentTimeMillis() - startTime));
    }



/**
     * 普危货导出任务
     */
    class ExportTask implements Runnable {

        /**
         * poi对象
         */
        private Workbook workbook;
        /**
         * 查询偏移量
         */
        private Integer offset;
        /**
         * sheet页后缀
         */
        private String sheetSuffix;
        /**
         * 类型,0危化无单 1普货进园
         */
        private Integer type;

        public ExportTask(Workbook workbook, Integer offset, String sheetSuffix, Integer type) {
            this.workbook = workbook;
            this.offset = offset;
            this.sheetSuffix = sheetSuffix;
            this.type = type;
        }

        @Override
        public void run() {
            List<OrderPassApplyDo> list = orderPassApplyMapper.listExportDataByPage(type, offset, THREAD_THRESHOLD);
            list.forEach(orderPassApplyDo -> {
                Sheet sheet = workbook.getSheet(orderPassApplyDo.getCreatedMonth() + sheetSuffix);
                Row row = createRow(sheet);
                packageExportData(row, orderPassApplyDo);
            });
            // 计数减一
            countDownLatch.countDown();
        }
    }

    private Row createRow(Sheet sheet) {
        Row row;
        synchronized (lockMap.computeIfAbsent(sheet, k -> new Object())) {
            int rowNum = sheet.getLastRowNum();
            row = sheet.createRow(rowNum + 1);
        }
        return row;
    }

    private void packageExportData(Row row, OrderPassApplyDo orderPassApplyDo) {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        int cellIndex = 0;
        Cell cell0 = row.createCell(cellIndex++);
        cell0.setCellValue(orderPassApplyDo.getUnloadEntName());

        Cell cell1 = row.createCell(cellIndex++);
        cell1.setCellValue(orderPassApplyDo.getLoadingEntName());

        Cell cell2 = row.createCell(cellIndex++);
        cell2.setCellValue(orderPassApplyDo.getOrderType() == 0 ? "进园装货" : "进园卸货");

        Cell cell3 = row.createCell(cellIndex++);
        cell3.setCellValue(sdf.format(orderPassApplyDo.getCreatedTime()));

        Cell cell4 = row.createCell(cellIndex++);
        cell4.setCellValue(orderPassApplyDo.getTractorPlateNum());

        Cell cell5 = row.createCell(cellIndex++);
        cell5.setCellValue(orderPassApplyDo.getTrailerPlateNum());

        Cell cell6 = row.createCell(cellIndex++);
        cell6.setCellValue(orderPassApplyDo.getDriverName());

        Cell cell7 = row.createCell(cellIndex++);
        cell7.setCellValue(orderPassApplyDo.getDriverIdCard());

        Cell cell8 = row.createCell(cellIndex++);
        cell8.setCellValue(orderPassApplyDo.getDriverContact());

        Cell cell9 = row.createCell(cellIndex++);
        cell9.setCellValue(orderPassApplyDo.getLoadingEntPerson());

        Cell cell10 = row.createCell(cellIndex++);
        cell10.setCellValue(orderPassApplyDo.getLoadingEntContact());

        Cell cell11 = row.createCell(cellIndex++);
        cell11.setCellValue(orderPassApplyDo.getUnloadEntPerson());

        Cell cell12 = row.createCell(cellIndex++);
        cell12.setCellValue(orderPassApplyDo.getUnloadEntContact());

        Cell cell13 = row.createCell(cellIndex++);
        cell13.setCellValue(orderPassApplyDo.getNote());

        Cell cell14 = row.createCell(cellIndex++);
        cell14.setCellValue(sdf.format(orderPassApplyDo.getPlanStartTime()));

        Cell cell15 = row.createCell(cellIndex++);
        cell15.setCellValue(sdf.format(orderPassApplyDo.getPlanEndTime()));

        Cell cell16 = row.createCell(cellIndex++);
        cell16.setCellValue(null != orderPassApplyDo.getActualStartTime() ? sdf.format(orderPassApplyDo.getActualStartTime()) : null);

        Cell cell17 = row.createCell(cellIndex++);
        cell17.setCellValue(null != orderPassApplyDo.getActualEndTime() ? sdf.format(orderPassApplyDo.getActualEndTime()) : null);

        Cell cell18 = row.createCell(cellIndex++);
        cell18.setCellValue(null != orderPassApplyDo.getAuditTime() ? sdf.format(orderPassApplyDo.getAuditTime()) : null);

        Cell cell19 = row.createCell(cellIndex++);
        cell19.setCellValue(orderPassApplyDo.getAuditComment());

        Cell cell20 = row.createCell(cellIndex++);
        cell20.setCellValue(orderPassApplyDo.getAuditorName());

        Cell cell21 = row.createCell(cellIndex);
        String status = null;
        switch (orderPassApplyDo.getStatus()) {
            case AUDIT_PENDING:
                status = "审核中";
                break;
            case AUDIT_SUCCESS:
                status = "审核通过";
                break;
            case AUDIT_FAILED:
                status = "审核不通过";
                break;
            case APPLY_AGAIN_FOR_LATE:
                status = "迟到再申请";
                break;
            case APPLY_INVALIDATION:
                status = "失效";
                break;
        }
        cell21.setCellValue(status);
    }

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

清茶_

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

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

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

打赏作者

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

抵扣说明:

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

余额充值