百万数据导出+上传文件服务器+下载

背景描述:数据库里面有一张表A,里面有90w+的数据,有时候会达到上百万。我想从前端页面通过点击“导出”按钮导出数据。

面临的问题及思路:

1.从数据库中查90w+数据时间太久,会导致前端超时,用户体验不好

2.一张表存90w+数据不现实,必定要拆分

3.每次用户导出都要查询数据库→下载,用户体验不好,所以第一次生成文件以后把文件放到文件服务器上,然后返回文件地址,以后每次导出只需要通过地址下载存在文件服务器上的文件即可

流程图如下:

百万数据导出+上传文件服务器+下载_第1张图片

代码简要:

1.Controller层

@ApiOperation("导出")
@GetMapping("output/exports")
@ResponseBody
@PreAuthorize("isAuthenticated()")
Output  OutputsExport(HttpServletResponse response,Input Input) throws IOException {
    //查表,看有没有地址
    DataDownloadUrl dataDownloadUrl=dataDownloadUrlRepository.findByName("xxx");
    if(如果数据库中有地址){
        return 地址;
    } else{
       //起一个线程,用于生成excel。另一方面先返回给用户一个状态
        Thread ts=new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    //调用Service层
              service.excelOutput(Input,prefix);  //prefix是上传文件服务器所要用到的东西
                } catch (IOException e) {
                    e.printStackTrace();
                } catch (ExecutionException e) {
                    e.printStackTrace();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
        ts.start();
        return new Output("正在准备数据,由于数据量较大,请于30分钟之后进行下载!");
    }

}

2.Service层(加粗加下划线标红的方法后续列出来)

public Output excelOutput(Input Input,String prefix) throws Exception {
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
    Integer count=mydataRepository.countNum();  //计算总共要导出的数据量
    Integer pageSize=100000; //每一个excel存的数据量
    //根据数据总量以及设定好的每个excel存的数据量计算excel个数
    Integer excelNum=((count%pageSize==0)?(count/pageSize) : (count /pageSize + 1));
    Integer writeSize=1000;  //分批写入数据量为count的总数据,每次向excel中写入的数量为writeSize
     Integer writeNum=pageSize/writeSize; //计算每个excel的写入次数
    //标题
    String[] titles={"主键ID","姓名","学号","成绩"};
    String path = "学生信息"+sdf.format(new Date()); //定义保存excel的文件夹
    for(int i=0;icreateWord2007(titles); //创建excel,并把表头写好
        SXSSFSheet sheet=wb.getSheetAt(0);//拿到第一个sheet
        for(int j=1;j<=writeNum;j++){ //循环向excel中写入数据,每个excel的数据量为pageSize
            int startRow=(j-1)*writeSize+1; //每次获取的起始行
            int endRow=startRow+writeSize-1; //每次获取的最后一行
            //分页查询,每次查询1000条数据
            Page myDatas= myDataRepository.findAll(new PageRequest(writeNum*i+j-1,1000));
            List myDataList=myDatas.getContent();//Page转List
            if(!ObjectUtils.isEmpty(myDataList)){  //写入excel
                for(int x=startRow;x<=endRow;x++){
                    SXSSFRow eachDataRow = sheet.createRow(x);
                    if ((x-startRow) < myDataList.size()) {
                        MyData excelData = myDataList.get(x - startRow);

                        eachDataRow=MyDataEncap.ExcelOutputTransfer(eachDataRow,excelData);
                    }
                }

            }
        }  //for循环结束
        //把生成的excel文件命好名以后保存到我们之前定义的文件夹中
        DownloadUtil.downLoadExcelToLocalPath(wb, path+ "/"+fileName, i == excelNum - 1 ? true : false);
    }
    //把文件夹打包
    ZipUtils.doCompress(path,  "学生信息"+sdf.format(new Date())+".zip");    //zip文件转流,准备上传至文件服务器
    File file=new File("学生信息"+sdf.format(new Date())+".zip");
    InputStream inputStream = new FileInputStream(file);
    //定义一个字符串,用于接收文件服务器返回的地址
    String fileUrlFromFileOS = "";
    byte[] byt = new byte[inputStream.available()];
    inputStream.read(byt);
    ByteArrayInputStream bis = new ByteArrayInputStream(byt);
    //丢到服务器上面去,就是下面的save()方法,用fileUrlFromFileOS接收地址
    fileUrlFromFileOS = prefix+"/"+new ElianUploadUtil().save(bis);
    if(fileUrlFromFileOS!=null){
        String arr[]=  fileUrlFromFileOS.split(":");
        fileUrlFromFileOS="https:"+arr[1];
    }
   //下面是将地址保存到数据库中。可以忽略不用看了
    if(!ObjectUtils.isEmpty(dataDownloadUrl)){
        dataDownloadUrl.setUpdatetime(mydataPrice.getUpdatetime());
        dataDownloadUrl.setUrl(fileUrlFromFileOS);
        dataDownloadUrl.setStatus(0);
        dataDownloadUrlRepository.save(dataDownloadUrl);
    }else{
        DataDownloadUrl dataDownloadUrl1=new DataDownloadUrl ();
        dataDownloadUrl1.setUrl(fileUrlFromFileOS);
    dataDownloadUrl1.setUpdatetime(mydata.getUpdatetime());
    dataDownloadUrl1.setModuleName("studentInfo");
        dataDownloadUrl1.setStatus(0);
    dataDownloadUrl1Repository.save(dataDownloadUrl1);
    }
    System.out.println(fileUrlFromFileOS);

    return new Output();
}

 

3.service里用到的方法

(1)createWord2007

public static SXSSFWorkbook createWord2007(String[] titles) {
    // 在内存当中保持 100 行 , 超过的数据放到硬盘中在内存当中保持 100 行 , 超过的数据放到硬盘中
    SXSSFWorkbook wb = new SXSSFWorkbook(100);

    // 根据总记录数创建sheet并分配标题
    SXSSFSheet sheet = wb.createSheet();
    SXSSFRow headRow = sheet.createRow(0);
    for (int j = 0; j < titles.length; j++) {
        SXSSFCell headRowCell = headRow.createCell(j);
        headRowCell.setCellValue(titles[j]);
    }
    return wb;
}

(2)ExcelOutputTransfer

public static SXSSFRow ExcelOutputTransfer(SXSSFRow eachDataRow,MyData myData) {
    eachDataRow.createCell(0).setCellValue(myData.getQyid());  //"主键ID"
    eachDataRow.createCell(1).setCellValue(myData.getStddrugidnew());  //"姓名"
    eachDataRow.createCell(2).setCellValue(myData.getProductname());  //"学号"
    eachDataRow.createCell(3).setCellValue(myData.getDrugform()); //"成绩"
    return eachDataRow;
}

(3)downLoadExcelToLocalPath

public static void downLoadExcelToLocalPath(SXSSFWorkbook wb, String exportPath,boolean end) {
    FileOutputStream fops = null;
    try {
        fops = new FileOutputStream(exportPath);
        wb.write(fops);
    } catch (Exception e) {
        e.printStackTrace();
    } finally {
        if(end){
            if (null != wb) {
                try {
                    wb.dispose();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
            if (null != fops) {
                try {
                    fops.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

(4)doCompress(这里直接放zipUtil工具类。百度都能百度到的)

public class ZipUtils {
    private ZipUtils(){
    }

    public static void doCompress(String srcFile, String zipFile) throws IOException {
        doCompress(new File(srcFile), new File(zipFile));
    }

    /**
     * 文件压缩
     * @param srcFile 目录或者单个文件
     * @param zipFile 压缩后的ZIP文件
     */
    public static void doCompress(File srcFile, File zipFile) throws IOException {
        ZipOutputStream out = null;
        try {
            out = new ZipOutputStream(new FileOutputStream(zipFile));
            doCompress(srcFile, out);
        } catch (Exception e) {
            throw e;
        } finally {
            out.close();//记得关闭资源
        }
    }

    public static void doCompress(String filelName, ZipOutputStream out) throws IOException{
        doCompress(new File(filelName), out);
    }

    public static void doCompress(File file, ZipOutputStream out) throws IOException{
        doCompress(file, out, "");
    }

    public static void doCompress(File inFile, ZipOutputStream out, String dir) throws IOException {
        if ( inFile.isDirectory() ) {
            File[] files = inFile.listFiles();
            if (files!=null && files.length>0) {
                for (File file : files) {
                    String name = inFile.getName();
                    if (!"".equals(dir)) {
                        name = dir + "/" + name;
                    }
                    ZipUtils.doCompress(file, out, name);
                }
            }
        } else {
            ZipUtils.doZip(inFile, out, dir);
        }
    }

    public static void doZip(File inFile, ZipOutputStream out, String dir) throws IOException {
        String entryName = null;
        if (!"".equals(dir)) {
            entryName = dir + "/" + inFile.getName();
        } else {
            entryName = inFile.getName();
        }
        ZipEntry entry = new ZipEntry(entryName);
        out.putNextEntry(entry);

        int len = 0 ;
        byte[] buffer = new byte[1024];
        FileInputStream fis = new FileInputStream(inFile);
        while ((len = fis.read(buffer)) > 0) {
            out.write(buffer, 0, len);
            out.flush();
        }
        out.closeEntry();
        fis.close();
    }


}

(5)save(上传文件到文件服务器上,这里也直接贴类的代码了)

public class ElianUploadUtil {
    static Logger logger = LoggerFactory.getLogger(ElianUploadUtil.class);
    @Value("${xxxxxxxx}")
    private  String endpoint="xxxxxxxxxx";
    @Value("${xxxxxxxxxx}")
    private  String accessKeyId="xxxxxxxxxx";
    @Value("${xxxxxxxxxx}")
    private  String accessKeySecret="xxxxxxxxxx";
    @Value("${xxxxxxxxxx}")
    private  String bucketName="xxxxxxxxxx";
    @Value("${xxxxxxxxxx}")
    private  String bucketNameTwo="xxxxxxxxxx";
    @Value("${xxxxxxxxxx}")
    private  String prefix="";
//上面的参数我用xxxx代替了,具体的可以自行百度看一下文件服务器都是怎么配置的

    public String save(ByteArrayInputStream byteArrayInputStream) throws Exception {
        // 1.创建OSSClient实例
        OSSClient ossClient = new OSSClient(endpoint, accessKeyId, accessKeySecret);
        // 2. 上传后文件名
        StringBuilder filename= new StringBuilder("MyData");
        String key =filename+".zip";
        // 3.上传文件
        ossClient.putObject(bucketName, key, byteArrayInputStream);
        // 4 关闭client
        ossClient.shutdown();
        return prefix.concat(key);
    }


    //转流文件
    public static ByteArrayInputStream workbookConvertorStream(SXSSFWorkbook workbook) {
        ByteArrayInputStream in = null;
        try{
            //临时缓冲区
            ByteArrayOutputStream out = new ByteArrayOutputStream();
            //创建临时文件
            workbook.write(out);
            byte [] bookByteAry = out.toByteArray();
            in = new ByteArrayInputStream(bookByteAry);
        }
        catch (Exception e){
            e.printStackTrace();
        }
        return in;
    }
}

 

注:以上代码仅仅是个人在项目中遇到瓶颈进行的技术记录。也许有更好的导出方案,但是个人水平有限,仅能做到这些。且上面有很多技术点我还没有吃透,所以仅仅做个记录,不敢深入分析乱点江山。

你可能感兴趣的:(百万数据导出+上传文件服务器+下载)