来源:程序员小富 动态数据导出是避免一般项目都会涉及到的功能。它的效导现方基本实现逻辑就是从mysql查询数据,加载到内存,出百然后从内存创建excel或者csv,数据t实以流的避免形式响应给前端。 SpringBoot下载excel基本都是出百这么干。 虽然这是数据t实个可行的方案,然而一旦mysql数据量太大,避免达到十万级,效导现方百万级,出百千万级,数据t实大规模数据加载到内存必然会引起OutofMemoryError。避免 要考虑如何避免OOM,效导现方一般有两个方面的出百思路。 一方面就是尽量不做呗,先怼产品下面几个问题啊: 如果产品说 “甲方是爸爸,你去和甲方说啊”,“客户说这个做出来,才考虑付尾款!”,如果客户的确缺根筋要让你这样搞, 那就只能从技术上考虑如何实现了。 从技术上讲,为了避免OOM,我们一定要注意一个原则: 不能将全量数据一次性加载到内存之中。 全量加载不可行,那我们的目标就是如何实现数据的分批加载了。实事上,Mysql本身支持Stream查询,我们可以通过Stream流获取数据,然后将数据逐条刷入到文件中,每次刷入文件后再从内存中移除这条数据,从而避免OOM。 由于采用了数据逐条刷入文件,而且数据量达到百万级,所以文件格式就不要采用excel了,excel2007最大才支持104万行的数据。这里推荐: 以csv代替excel。 考虑到当前SpringBoot持久层框架通常为JPA和mybatis,云南idc服务商我们可以分别从这两个框架实现百万级数据导出的方案。 实现项目对应: 核心注解如下,需要加入到具体的Repository之上。方法的返回类型定义成Stream。Integer.MIN_VALUE告诉jdbc driver逐条返回数据。 + Integer.MIN_VALUE)) ) ; 此外还需要在Stream处理数据的方法之上添加@Transactional(readOnly = true),保证事物是只读的。 同时需要注入javax.persistence.EntityManager,通过detach从内存中移除已经使用后的对象。 , method = RequestMethod.GET) ) { ); ); ); (Stream PrintWriter out = response.getWriter(); todoStream.forEach(rethrowConsumer(todo -> { String line = todoToCSV(todo); out.write(line); ); entityManager.detach(todo); })); out.flush(); (IOException e) { + e.getMessage(), e); , e); } } MyBatis实现逐条获取数据,必须要自定义ResultHandler,然后在mapper.xml文件中,对应的select语句中添加fetchSize="-2147483648"。 最后将自定义的ResultHandler传给SqlSession来执行查询,并将返回的结果进行处理。 以下是基于MyBatis Stream导出的完整的工程样例,我们将通过对比Stream文件导出和传统方式导出的内存占用率的服务器租用差异,来验证Stream文件导出的有效性。 我们先定义一个工具类DownloadProcessor,它内部封装一个HttpServletResponse对象,用来将对象写入到csv。 { HttpServletResponse response; { .response = response; ; ); +fileName); ); } { { ); (IOException e){ e.printStackTrace(); } } } 然后通过实现org.apache.ibatis.session.ResultHandler,自定义我们的ResultHandler,它用于获取java对象,然后传递给上面的DownloadProcessor处理类进行写文件操作: { DownloadProcessor downloadProcessor; ( { (); .downloadProcessor = downloadProcessor; } { Authors authors = (Authors)resultContext.getResultObject(); downloadProcessor.processData(authors); } } 实体类: { Integer id; String firstName; String lastName; String email; Date birthdate; Date added; { id; } { .id = id; } { firstName; } { : firstName.trim(); } { lastName; } { : lastName.trim(); } { email; } { : email.trim(); } { birthdate; } { .birthdate = birthdate; } { added; } { .added = added; } { .added; } } Mapper接口: { ; } Mapper xml文件核心片段,以下两条select的唯一差异就是在stream获取数据的方式中多了一条属性:fetchSize="-2147483648" select distinct false as QUERYID, from authors order by ${ orderByClause} select distinct false as QUERYID, from authors order by ${ orderByClause} 获取数据的核心service如下,由于只做个简单演示,就懒得写成接口了。其中 streamDownload 方法即为stream取数据写文件的实现,它将以很低的内存占用从MySQL获取数据;此外还提供traditionDownload方法,它是一种传统的下载方式,批量获取全部数据,然后将每个对象写入文件。 { SqlSessionTemplate sqlSessionTemplate; AuthorsMapper authorsMapper; { .sqlSessionTemplate = sqlSessionTemplate; .authorsMapper = authorsMapper; } / * stream读数据写文件方式 httpServletResponse IOException { AuthorsExample(); authorsExample.createCriteria(); HashMap<>(); , authorsExample.getOredCriteria()); , authorsExample.getOrderByClause()); DownloadProcessor (httpServletResponse)); sqlSessionTemplate.select( , param, customResultHandler); httpServletResponse.getWriter().flush(); httpServletResponse.getWriter().close(); } / * 传统下载方式 httpServletResponse IOException { AuthorsExample(); authorsExample.createCriteria(); List authors = authorsMapper.selectByExample (authorsExample); DownloadProcessor (httpServletResponse); authors.forEach (downloadProcessor::processData); httpServletResponse.getWriter().flush(); httpServletResponse.getWriter().close(); } } 下载的入口controller: ) { AuthorsService authorsService; { .authorsService = authorsService; } ) { authorsService.streamDownload(response); } ) { authorsService.traditionDownload (response); } } 实体类对应的表结构创建语句: ( AUTO_INCREMENT, , , , , , ) =utf8_unicode_ci; 这里有个问题:如何短时间内创建大批量测试数据到MySQL呢?一种方式是使用存储过程 + 大杀器 select insert 语句!不太懂? 没关系,且看我另一篇文章 MySQL如何生成大批量测试数据 你就会明白了。如果你懒得看,我这里已经将生成的270多万条测试数据上传到网盘,你直接下载然后通过navicat导入就好了。 有了测试数据,我们就可以直接测试了。先启动项目,然后打开jdk bin目录下的 jconsole.exe 首先我们测试传统方式下载文件的内存占用,直接浏览器访问:http://localhost:8080/download/traditionDownload。 可以看出,下载开始前内存占用大概为几十M,下载开始后内存占用急速上升,峰值达到接近2.5G,即使是下载完成,堆内存也维持一个较高的占用,这实在是太可怕了,如果生产环境敢这么搞,不出意外肯定内存溢出。 接着我们测试stream方式文件下载的内存占用,浏览器访问:http://localhost:8080/download/streamDownload,当下载开始后,内存占用也会有一个明显的上升,但是峰值才到500M。对比于上面的方式,内存占用率足足降低了80%!怎么样,兴奋了吗! 我们再通过记事本打开下载后的两个文件,发现内容没有缺斤少两,都是2727127行,完美! 感谢阅读,希望对你有所帮助 :) 来源: blog.csdn.net/haohao_ding/article/details/123164771前言
JPA实现百万级数据导出
具体方案不妨参考:http://knes1.github.io/blog/2015/2015-10-19-streaming-mysql-results-using-java8-streams-and-spring-data.html。MyBatis实现百万级数据导出
MyBatis实现百万级数据导出的具体实例