将海量动态数据以 JSON 格式导出

1,064 阅读1分钟

先点赞再看,养成好习惯

本文正在参加「Java主题月 - Java Debug笔记活动」,详情查看 活动链接

前言

如果想一次性导出海量数据为 JSON 文件(不要问我为什么有这种需求),一次性全部查询出来然后通过 JSON 库去序列化,一定是不现实的。因为如果一次导出的数据有 1GB,那么有 10 个人同时导出,实时内存占用就会达到 10 GB,而且还是不能回收的那种。 ​

再大的内存也扛不住这个玩法,所以需要换一种方式,增量导出。每次只获取一部分数据,写出到 JSON 串里,完成 JSON 串的写入,最后再进行输出(到文件或者其他 OutputStream)。 ​

但这样还是会有一个问题,虽说获取的数据是增量的,每次内存中只会存在这部分获取的数据;但写到 JSON 串这个操作,还是会导致所有数据堆积在内存中…… ​

强大的 Jackson

好在有足够强大的 Jackson,对于 JSON 序列化这个操作,它支持“增量”的模式。基于 SequenceWriter 来写入的话,可以做到每次只 write 一部分数据,在最后才执行 end 操作,完成结束的写入

// 记得维护单例
ObjectMapper jsonMapper = new ObjectMapper();

// 记得维护单例
ObjectWriter writer = jsonMapper.writer().withDefaultPrettyPrinter();

// 使用 writer.writeValues(OutputStream); 的方式,来创建 SequenceWriter
SequenceWriter sequenceWriter = writer.writeValues(outputstream);

for (int i = 0; i < 100; i++) {
    // 然后对 sequenceWriter 执行 writeAll,即可完成“增量”创建 json 串
	sequenceWriter.writeAll(createData(100));
}

// 最后一定要对 sequenceWriter close,才可以完成写入
sequenceWriter.close();


private List<Map<String,Object>> createData(int size){
    	List<Map<String,Object>> dataList = new ArrayList<>();
    for (int i = 0; i < size; i++) {
        Map<String,Object> dataMap = new HashMap<>();
        dataMap.put("A", RandomStringUtils.randomNumeric(10000));
        dataMap.put("B", RandomStringUtils.randomNumeric(10000));
        dataMap.put("C", RandomStringUtils.randomNumeric(10000));
        dataMap.put("D", RandomStringUtils.randomNumeric(10000));
        dataMap.put("E", RandomStringUtils.randomNumeric(10000));
        dataList.add(dataMap);
    }
    return dataList;
}

这样一来就避免了内存中存储全量数据的问题,增量导出的时候只有一组数据会驻留在内存中,再多的数据也没问题。

配合 Servlet/Spring MVC 中下载

基于上面的增量导出逻辑,如果在 Servlet 场景中完成这个增量数据导出 JSON 文件的功能呢? ​

由于数据是增量的,无法一次保存至内存中,所以根本不知道数据的总大小,Content-Length更是无法设置了。不过 HTTP 协议中还有一个 Chunked 编码格式,在 Chunked 下是不需要 Content-Length 的(关于 Chunked 编码可以参考juejin.cn/post/684490…),客户端只需要解析每个 Chunk 部分,即可获取全部报文。

@GetMapping("download")
public void download(HttpServletResponse httpServletResponse) throws IOException {
	
    // 直接给个文件名,不需要设置 Content-Length
    httpServletResponse.setHeader("Content-Disposition", "attchement;filename=" + "data.json");

    ObjectWriter writer = jsonMapper.writer().withDefaultPrettyPrinter();
	
    // 使用 servlet resp 作为 SequenceWriter outputstream
    SequenceWriter sequenceWriter = writer.writeValues(httpServletResponse.getOutputStream());

    for (int i = 0; i < 100; i++) {
        sequenceWriter.writeAll(createData(100));
    }

    sequenceWriter.close();
}

如果是在 Tomcat 下,当 OutputStream 写入的数据过多时(大于8K),会自动使用 Chunked 编码,无需手动设置

Jackson 可不止 JSON

Jackson 可能有些人认为它只是个 JSON 库,其实不止。它除了支持 JSON 序列化以外,还支持 XML/YAML/Properties/CSV 等等,而且 Jackson 是将基础功能进行了抽象,每一种序列化都是扩展的形式存在;所以上面的增量输出 SequenceWriter,也是支持其他格式的!

原创不易,禁止未授权的转载。如果我的文章对您有帮助,就请点赞/收藏/关注鼓励支持一下吧❤❤❤❤❤❤