JAVA大数据量导出问题

1问题背景

  • 最近公司在做一个导出的功能,是这样子的,公司内部有很多套系统,现在需要开发了一个门户系统,希望能连接各个系统,在门户系统上希望可以给其他子系统增加一个导出功能,支持对其他各个系统的数据导出。
  • 由此带来几个问题:
    1. 因为是生产系统,所以只能对需要使用该功能的系统 将其数据源配置在jboss 上(因种种安全上的考量,JDBC的直连方式不被允许)
    2. 只能对其他生产系统进行查操作,禁止一切修改
    3. 查询的sql语句由消费系统来写,由此带来的性能问题等引发了大数据量查询 不能造成本系统 服务压力,已经使用者系统压力的问题解决

2问题解决

        本篇主要对大数量查询导出问题作出记录,jboss数据源获取方式 和使用不做赘述。

   1 分页查询

    因为不确定sql 段落内容 和 数据量情况,采用每100条数据为一次查询,然后一万条数据写入一张表。

    

  //每页最大数
        int rowCount = 100;
        int pageCount ;

        int excelCount = 10000;
        int sign = 0;

        int searchCount ;
        //
        if (count % excelCount == 0) {
            pageCount = count / excelCount;
        } else {
            pageCount = count / excelCount + 1;
        }
        if (count % rowCount == 0) {
            searchCount = count / rowCount;
        } else {
            searchCount = count / rowCount + 1;
        }


        //列头
        List<String> headers = new ArrayList<>();

        //需要压缩的文件名
        String zipName = StringFormateUtil.getDateTime() + "";
        String tempPath = ReportTaskUtil.PATH_FILE + "/" + zipName;
        //判断临时文件夹
        File file = new File(tempPath);
        //新建一个临时文件夹
        ReportTaskUtil.judeDirExists(file);

        Map<String, Object> result = new HashMap<>();

        result.put("tempPath", tempPath);
        result.put("zipName", zipName);
        response.getData().setRows(result);
        for (int i = 0; i < pageCount; i++) {

            if(Thread.currentThread().isInterrupted()){
                throw new InterruptedException("线程强制中断");
            }

            //每一页对应一个 excel
            ExcelUtil07 excelUtil07 = null;
            //查 100 条每次,防止 查询过大 造成 系统压力
            for (int m = 0; m <100 ; m++) {

                    if(searchCount==0){
                        break;
                    }
                    searchCount--;

                String exeSql2 = String.format(" select * from (%s) a  limit %d, %d", sql, sign, rowCount);
                try (PreparedStatement preparedStatement2 = conn.prepareStatement(exeSql2);
                     ResultSet rs2 = preparedStatement2.executeQuery();) {
                    //查询列集合第一次
                    if(firstQuery){
                        //获取查询的字段
                        ResultSetMetaData rsmd = rs2.getMetaData();
                        headers = getHeaders(rsmd);
                        firstQuery = false;
                        //excel 格式 名称 等设置
                        excelUtil07 = new ExcelUtil07(i+"_"+reportName, headers);
                    }

                    sign += rowCount;
                    // 行的集合
                    List<List<String>> rows = new ArrayList<>();
                    while (rs2.next()) {
                        List<String> v = new ArrayList<>();
                        for (int j = 0; j < headers.size(); j++) {
                            v.add(rs2.getString(j + 1));
                        }
                        rows.add(v);
                    }
                    //每一百行 写入一次
                    if(!excelUtil07.create07Xlsx(rows)){
                        LOGGER.error("07excel 生成失败");
                        response.setSuccess(false);
                        response.setErrorMsg("07excel 生成失败");
                        return response;
                    }
                } catch (Exception e) {
                    LOGGER.error(" 执行异常", e);
                    response.setSuccess(false);
                    response.setErrorMsg(" 执行异常");
                    break;
                }
            }
            excelUtil07.export(tempPath);
            firstQuery  = true;
        }

    

   2 excel 内存问题

    为了尽可能减少内存的开销,防止同一时间存在太多对象,我们这里使用SXSSFWorkbook 类,该类接收一个int 类型的 参数,从源码可以知道 此参数可以控制内存中的对象,多余数量的对象会存在磁盘上

    @param rowAccessWindowSize the number of rows that are kept in memory until flushed out, see above.
package com.suning.bi.util;

import com.suning.bi.entity.ReportResponse;
import org.apache.poi.hssf.usermodel.HSSFCellStyle;
import org.apache.poi.hssf.usermodel.HSSFFont;
import org.apache.poi.hssf.util.HSSFColor;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.xssf.streaming.SXSSFCell;
import org.apache.poi.xssf.streaming.SXSSFRow;
import org.apache.poi.xssf.streaming.SXSSFSheet;
import org.apache.poi.xssf.streaming.SXSSFWorkbook;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

/**
 * @author 17103349
 */
public class ExcelUtil07 {

    private static Logger LOGGER = LoggerFactory.getLogger(ExcelUtil07.class);

    private SXSSFWorkbook sxssfWorkbook;
    private SXSSFCell sxssfCell;
    private SXSSFSheet sheet;
    private SXSSFRow sxssfRow;
    private String fileName;
    private int numStart = 0;

    /**
     * @param fileName 文件名
     * @param titles   标题头
     */
    public ExcelUtil07(String fileName, List<String> titles) {


        this.fileName = fileName;
        //设置Workbook 超出100 将缓存在磁盘,保证 row最多有100在内存中,防止内存开销过大
        sxssfWorkbook = new SXSSFWorkbook(100);
        sxssfCell = null;
        sxssfRow = null;
        sheet = sxssfWorkbook.createSheet(fileName);

        CellStyle style = sxssfWorkbook.createCellStyle();

        style.setFillForegroundColor(HSSFColor.SEA_GREEN.index);
        style.setFillPattern(HSSFCellStyle.SOLID_FOREGROUND);
        style.setAlignment(HorizontalAlignment.CENTER); // 居中

        style.setBorderBottom(HSSFCellStyle.BORDER_THIN);// 下边框
        style.setBorderLeft(HSSFCellStyle.BORDER_THIN);// 左边框
        style.setBorderRight(HSSFCellStyle.BORDER_THIN);// 右边框
        style.setBorderTop(HSSFCellStyle.BORDER_THIN);// 上边框


        Font font = sxssfWorkbook.createFont();
        font.setFontName("仿宋_GB2312");
        //粗体显示
        font.setBold(true);
        //字体大小
        font.setFontHeightInPoints((short) 12);

        style.setFont(font);
        //设置自动换行
        style.setWrapText(true);

        sxssfRow = sheet.createRow(0);

        //设置头
        for (int a = 0; a < titles.size(); a++) {
            sheet.setColumnWidth(a, 3766);

            SXSSFCell sxssfCell = sxssfRow.createCell(a);

            sxssfCell.setCellValue(titles.get(a));

            sxssfCell.setCellStyle(style);

        }
    }

    /**
     * 07版本excel 生成 (poi 3.8以后 有临时文件)
     *
     * @param dataLists 数据集
     * @return
     */
    public boolean create07Xlsx(List<List<String>> dataLists) {
        try {
            int size = dataLists.size();
            for (int a = 1; a <=size; a++) {

                sxssfRow = sheet.createRow(a+numStart);
                List<String> data = dataLists.get(a - 1);
                int cell = 0;
                for (int i = 0; i < data.size(); i++) {
                    Object valueObject = data.get(i);
                    sxssfCell = sxssfRow.createCell(cell);
                    sxssfCell.setCellValue(valueObject == null ? "" : valueObject.toString());
                    sxssfCell.getCellStyle().setWrapText(true);
                    cell++;
                }
                //数据清理
                data.clear();
            }
            numStart+=size;

            //数据清理 回收对象
            dataLists.clear();
            //设置空引用
        } catch (Exception e) {
            LOGGER.error("xlsx 文件{}生成失败", fileName, e);
            return false;
        }
        return true;
    }

    /**
     * 导出正式文件
     * @return
     */
    public boolean export(String path){
        try {
            FileOutputStream fos = new FileOutputStream(path + "/" + fileName+ReportTaskUtil.XLSX_TYPE);
            sxssfWorkbook.write(fos);
            fos.close();
            sxssfWorkbook.dispose();
        } catch (IOException e) {
            LOGGER.error("xlsx 正式文件{}生成失败", fileName, e);
            return false;
        }
        return true;
    }


}

  3 生成的多个文件,对其进行打包处理上传ftp服务器

  

public boolean fileToZip(String sourceFilePath, String zipFilePath, String fileName) {
        boolean flag = false;
        File sourceFile = new File(sourceFilePath);

        if (sourceFile.exists() == false) {
            LOGGER.info("待压缩的文件目录:" + sourceFilePath + "不存在.");
            sourceFile.mkdir(); // 新建目录
        }
        File zipFile = new File(zipFilePath + "/" + fileName + ".zip");
        if (zipFile.exists()) {
            LOGGER.info(zipFilePath + "目录下存在名字为:" + fileName + ".zip" + "打包文件.");
        } else {
            File[] sourceFiles = sourceFile.listFiles();
            if (null == sourceFiles || sourceFiles.length < 1) {
                LOGGER.info("待压缩的文件目录:" + sourceFilePath + "里面不存在文件,无需压缩.");
            } else {
                try (FileOutputStream fos = new FileOutputStream(zipFile); ZipOutputStream zos = new ZipOutputStream(new BufferedOutputStream(fos));) {
                    byte[] bufs = new byte[1024 * 10];
                    for (int i = 0; i < sourceFiles.length; i++) {
                        String name = sourceFiles[i].getName();
                        ZipEntry zipEntry = new ZipEntry(name);
                        zos.putNextEntry(zipEntry);
                        try (FileInputStream fis = new FileInputStream(sourceFiles[i]); BufferedInputStream bis = new BufferedInputStream(fis, 1024 * 10);) {
                            int read = 0;
                            while ((read = bis.read(bufs, 0, 1024 * 10)) != -1) {
                                zos.write(bufs, 0, read);
                            }
                        } catch (Exception e) {
                            flag = false;
                            LOGGER.error("IO异常", e);
                        }
                    }
                } catch (IOException e) {
                    flag = false;
                    LOGGER.error("IO异常", e);
                }
                flag = true;
            }
        }
        return flag;
    }

3 遗留问题

     这里有个遗留问题,因为笔者的任务序列 使用 队列 做的,笔者在有想过在遇到 sql 执行 效率和性能问题时,强制 终止线程 ,但是 对于 stop 和 interrupt 方式 没有考虑好,interrupt 不能保证 线程 能立刻马上终止,stop则会遇到线程不安全和 资源释放的问题,现在还在纠结 采用的方案。

     其次,因为其他生产系统不受控制,对于其他系统的数据库 压力问题 ,在执行过程中不可控 ,也比较糟心。

4 最后

    写到这里,基本就记录完毕了,笔者不是什么大神。。很普通,有什么不对和改进的地方,希望能和大家一起探讨学习