Export大數據量導出和打包


項目需求

​ 導出生成大批量數據的文件,一個Excel中最多存有五十萬條數據,查詢多余五十萬的數據寫多個Excel中。導出完成是生成的多個Excel文件打包壓縮成zip,而后更新導出記錄中的壓縮文件路徑。

​ 大數據量文件一般采用異步生成文件,導出時首先授權生成一個流水號,而后將數據攜帶流水號請求導出接口。

拋開實際業務,做成一個比較公共的導出功能。

參數說明

{
    "className": "ValideData",         //導出的數據的實體類,類中有別名和順序相關的注解
    "createUser": "",				//操作人
    "downLoadNo": "202203181504732568468066304",  	//下載流水號
    "fileName": "機卡綁定",          //文件名      fileName+HHmmssSSS.xlsx
    "keys": [						//redis key的數據,分批獲取數據
    ],
    "remark": "機卡綁定",				//備注(不關注)
    "type": "機卡綁定"					//導出類型(不關注)
}

坐標

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-compress</artifactId>
  <version>1.21</version>
</dependency>

注:拋開導出前的參數校驗,只關注導出操作 。

主要代碼

邏輯說明:

  1. 導出前將請求參數更新到導出記錄中。
  2. 類加載器加載需要導出數據的實體類
  3. 設置一個數據量指針,記錄到每個文件的數據量
  4. 達到閾值時指定文件寫出到磁盤並清緩。
  5. 重置數據量指針,新增一條文件記錄(循環)
  6. 數據量指針未到閾值時但數據已經查詢完成---->>寫入剩余數據
  7. 查詢該流水號的所有文件記錄
  8. 壓縮文件並返回壓縮文件地址
  9. 更新到導出記錄中

主流程

public void bigDataExport(PortDto dto) throws Exception {
    long start = System.currentTimeMillis();
    log.info("開始導出,批次號:<{}>, 開始時間:{}", dto.getDownLoadNo(), DateUtil.now());

    //修改導出記錄
    LambdaUpdateWrapper<PortDto> updateWrapper = new LambdaUpdateWrapper<>();
    updateWrapper.eq(PortDto::getDownLoadNo, dto.getDownLoadNo());
    //生成導出記錄
    int row = this.baseMapper.update(dto, updateWrapper);
    if (row > 0) {
        log.info("批次號:<{}>准備生成文件", dto.getDownLoadNo());
        try {
            Iterator<String> iterator = keys.iterator();
            Workbook workbook = null;
            ExportParams params = new ExportParams();

            //加載導出數據實體類
            Class<?> aClass = Class.forName(entityBasePackage + dto.getClassName());

            int element = 0;
            while (iterator.hasNext()) {
                String key = iterator.next();
                Collection<?> list = getList(key, aClass);
                element += list.size();
                workbook = ExcelExportUtil.exportBigExcel(params, aClass, list);

                //文件數據達到閾值
                if (element >= maxDataCount) {
                    String fileName = dto.getFileName() + "_" + DateUtil.format(new Date(),
                            "HHmmssSSS") + ".xlsx";
                    ExcelExportUtil.closeExportBigExcel();
                    FileOutputStream fos =
                            new FileOutputStream(fileProp.getPath().getPath() + fileName);
                    workbook.write(fos);
                    fos.close();
                    element = 0;
                    //更新地址
                    Map<String, Object> map = new HashMap<>();
                    map.put("downloadNo", dto.getDownLoadNo());
                    map.put("filePath", fileProp.getPath().getPath() + fileName);
                    map.put("createTime", new Date());
                    this.baseMapper.insertPathRecord(map);
                    log.info("文件寫入完成,文件名:{}", fileName);
                    continue;
                }
                iterator.remove();
            }

            //寫入剩余文件
            if (element != 0) {
                String fileName = dto.getFileName() + "_" + DateUtil.format(new Date(),
                        "HHmmssSSS") + ".xlsx";
                ExcelExportUtil.closeExportBigExcel();
                FileOutputStream fos = new FileOutputStream(fileProp.getPath().getPath() + fileName);
                workbook.write(fos);
                fos.close();
                element = 0;
                //更新地址
                Map<String, Object> map = new HashMap<>();
                map.put("downloadNo", dto.getDownLoadNo());
                map.put("filePath", fileProp.getPath().getPath() + fileName);
                map.put("createTime", new Date());
                this.baseMapper.insertPathRecord(map);
                log.info("文件寫入完成,文件名:{}", fileName);
            }

            long end = System.currentTimeMillis();
            log.info("導出結束,批次號:<{}>, 結束時間:{}, 耗時:{}", dto.getDownLoadNo(), DateTime.of(end),
                    DateUtil.formatBetween(end - start));
        } catch (Exception e) {
            log.info("批次號<{}>導出異常:", dto.getDownLoadNo(), e);
            throw new BusinessException("");
        } finally {
            log.info("批次號<{}>生成文件結束,准備壓縮文件,修改狀態", dto.getDownLoadNo());
            //合並文件到導出文件記錄主表
            //當只有一個文件記錄時直接更新主表文件地址
            List<PortDto> recordList = exportDao.getPathRecord(dto);
            if (recordList.size() > 1) {
                //zipPath
                dto.setFilePath(zcat(dto, recordList));
            } else {
                //xlsxPath
                dto.setFilePath(recordList.size()==0? "":recordList.get(0).getFilePath());
            }
            updateWrapper.clear();
            updateWrapper.set(PortDto::getFilePath, dto.getFilePath());
            updateWrapper.set(PortDto::getSuccessTime, new Date());
            updateWrapper.set(PortDto::getStatus, "1");
            updateWrapper.eq(PortDto::getDownLoadNo, dto.getDownLoadNo());
            this.baseMapper.update(null, updateWrapper);
            log.info("批次號<{}>更新下載記錄表文件地址,修改狀態成功", dto.getDownLoadNo());
        }
    }
}

文件壓縮

/**
 *  多文件壓縮
 * @param dto 導出信息
 * @Param recordList 文件路徑
 * @return void
 * @throws
 * @author Surpass
 * @date 2022/3/17 9:59
 */
private String zcat(PortDto dto, List<PortDto> recordList) throws Exception {
    String fileName = dto.getFileName() + "_" + DateUtil.format(new Date(), "HHmmssSSS") + ".zip";
    String zipPath = fileProp.getPath().getPath() + fileName;
    Archiver archiver = CompressUtil.createArchiver(
            CharsetUtil.CHARSET_UTF_8,
            ArchiveStreamFactory.ZIP,
            new File(zipPath)
    );
    for (PortDto portDto : recordList) {
        archiver.add(FileUtil.file(portDto.getFilePath()));
    }
    archiver.finish();
    archiver.close();
    return zipPath;
}

查詢數據

/**
 *   查詢redis數據
 * @param key
 * @param cls
 * @return java.util.Collection<?>
 * @throws
 * @author Surpass
 * @date 2022/3/18 15:51
 */
private Collection<?> getList(String key, Class<?> cls) {
    List<String> list = redis.getList(key);
    return list.stream().map(item -> JSONObject.parseObject(item, cls)).collect(Collectors.toList());
}

補充

導出還設置了隊列計數器來限制同一時間最大的導出請求,使用aop在申請流水號時計數器+1,導出完成或者異常時隊列計數器-1。導出完成后根據操作人發送郵件通知導出結果。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM