场景是查询数据然后导出excel,接口响应太慢。

处理接口慢,首先要找出哪个环节慢。

打日志看各环节花费时间,10W条数据 关联查询sql花费 3s,导出excel花费5S,响应传输时间很快不是关键问题。

网上导出excel框架有很多,easyExcel(阿里巴巴,占用内存小),easyPoi (API丰富),POI,jacob等。一样一样看。

1.EasyPoi

经查看,发现EasyPoi框架中的导出大文件API,其实是为了防止内存溢出,因为原本数据文件都被加载到内存中,并发导出大文件容易内存溢出,所以会将临时数据写入磁盘防止这个问题,而EasyExcel 天生就做到这一点。

但我们的目的是加快接口响应速度。

2.EasyExcel

后来又查到一种,利用easyexcel分页sheet导出的方法,比如10W条数据,1W导出到一个sheet页,利用EasyExcel可以做到这一点。如:

但这个方法经试验,其实每个sheet间依然是串行的,并不能加快接口速度。

我尝试用多线程并发导出各个sheet失败了,poi 对单个文件的多sheet写并不支持多线程,会重复写到同一sheet上,程序报错。

3.JACOB

还有JACOB, JAVA-COM Bridge的缩写,(提供自动化的访问com的功能,也是通过JNI功能访问windows平台下的com组件或者win32系统库的)。这是利用C完成对office的操作,之前用过它转码PPT为图片,很快,但只能运行在windows上,虽然快但是不满足需求。

4.HuTool

Hutool工具类中也有BigExcelWriter,用于大文件写出,但还是只解决内存溢出的问题。

ZIPUtil,压缩文件,我们的问题在写,不是在于数据传输,所以也没用。

5.CSV

最后发现,有一种最快的方式,就是将文件导出为CSV格式。

其实POI是针对每个单元格去写,包含样式字体宽度等等,不仅IO任务重,计算任务也重,如果是csv,直接是文本的写,其实就是直接用Printer流去写一行一行的字符,非常快,但缺点是不能在程序中进行单元格样式调整。

  • EasyPOI中有CSV的API,但我试验失败了,最后使用的 阿帕奇的Commoms-csv。
  • 而之前的关联5表查询sql,改成了多次进入数据库单表查询,时间由3s变为2s.
  • 另外,csv导出时,必须将编码设置为GBK,否则用excel打开时中文乱码,(但这样用其他软件打开时乱码,先不管了)。网上有一种将在文件最前面写入几个字节作bom,使excel软件识别文件的编码方式为utf
public void logDownload(@RequestBody LogExportParam param, HttpServletResponse response) throws IOException, InterruptedException {
        response.setContentType("application/csv");
        response.setCharacterEncoding("gbk");
        String fileName = "log" + System.currentTimeMillis() + ".csv";
        response.setHeader("Content-disposition", "attachment;filename=" + fileName);
        //设置编码格式,否则中文乱码
        String encoding = "GBK";
        Writer outputStreamWriter = new BufferedWriter(new OutputStreamWriter(response.getOutputStream(), encoding));
        CSVFormat csvFormat = CSVFormat.DEFAULT.withHeader("序号", "日志类型", "描述", "校区", "教学楼", "教室", "操作时间", "操作人");
        CSVPrinter csvPrinter = new CSVPrinter(outputStreamWriter, csvFormat);

        logger.info("开始查询数据" + new Date().getMinutes() + "n" + new Date().getSeconds() + "s");
        List<ClassroomList> classroomList = classroomInfoService.classroomList(param.getSchoolId(), param.getTeachingBuildingId(), param.getClassroomId());
        if (CollectionUtils.isEmpty(classroomList))
            return;

        List<Long> classroomIds = classroomList.stream().map(ClassroomList::getClassroomId).collect(Collectors.toList());
        Map<Long, ClassroomList> classroomMap = classroomList.stream().collect(Collectors.toMap(ClassroomList::getClassroomId, Function.identity()));
        List<LogListDto> data = logService.logExportData(param, classroomIds);
        List<Long> userIds = data.stream().map(LogListDto::getCreateUserId).distinct().collect(Collectors.toList());
        Map<Long, String> usernameMap = authService.list(new QueryWrapper<IotSysUser>().select("username", "user_id").in("user_id", userIds)).stream().collect(Collectors.toMap(IotSysUser::getUserId, IotSysUser::getUsername));
        for (LogListDto log : data) {
            ClassroomList classroom = classroomMap.get(log.getClassroomId());
            log.setClassroomName(classroom.getClassroomName());
            log.setSchoolName(classroom.getSchoolName());
            log.setTeachingBuildingName(classroom.getTeachingBuildingName());
            log.setCreateUserName(usernameMap.get(log.getCreateUserId()));
        }

        logger.info("开始写文件" + new Date().getMinutes() + "n" + new Date().getSeconds() + "s");

        ListIterator<LogListDto> iterator = data.listIterator();
        while (iterator.hasNext()) {
            int index = iterator.nextIndex();
            LogListDto log = iterator.next();
            log.setSort(String.valueOf(index));
            csvPrinter.printRecord(log.getSort(), log.getLogType(), log.getRemark(), log.getSchoolName(), log.getTeachingBuildingName(), log.getClassroomName(), log.getCreateTime(), log.getCreateUserName());
        }
        logger.info("写出结束" + new Date().getMinutes() + "n" + new Date().getSeconds() + "s");

        data.clear();

        outputStreamWriter.flush();
        outputStreamWriter.close();

    }