话说笔者最近在做一个数据迁移的功能,说白了就是数据导出导入嘛。寻思着这还不简单,没想到第一步就GG了。
具体就是在数据库查询返回数据集时,由于数据集过大(大约几十万条吧)导致出现了内存溢出,即后台报错OOM。面对这种问题我们的第一反应肯定是将结果分页返回。但是分页却不得不考虑到几个问题:首先就是每页的大小,需要经过测试来确定一个合适的值,如果页面过大仍然可能出现内存溢出的问题;其次就是对于结果超大的数据集,在分页时偏移性能、查询开销和结果预加载似乎真的会损害分页方法的效率,简而言之就是会比较慢。
经过一翻查找,总算找到了比较适合的方法,即流式查询。这种查询是在java8进行支持的,所以首先需要我们的jdk在8以上。
先说使用效果吧,将50多万条数据查询并写入csv文件,以及下载,所需时间不足1分钟。
那么,它是如何实现的呢。很简单。
在使用 MySQL 时,为了真正流式传输结果,我们需要满足三个条件:
①设置结果集游标只能向前(Spring默认了)
②只读(设置@Transactional(readOnly = true)即可)
③提取大小设置为 Integer.MIN_VALUE(解释一下,这代表逐行读取)
具体步骤使用如下:
第一步,先在repository中写sql查询数据,返回类型为Stream
@QueryHints(value = @QueryHint(name = HINT_FETCH_SIZE, value = "" + Integer.MIN_VALUE))
@Query(value = "select t from device t")
Stream<Device> streamAll();
第二步:service方法中加入@Transactional(readOnly = true),并调用第一步查询方法
@Autowired
private EntityManager entityManager;
@GetMapping(value = "/csv")
@Transactional(readOnly = true)
public void exportTodosCSV(HttpServletResponse response) {
response.addHeader("Content-Type", "application/csv");
response.addHeader("Content-Disposition", "attachment; filename=todos.csv");
response.setCharacterEncoding("UTF-8");
try(Stream<Device> todoStream = deviceRepository.streamAll()) {
PrintWriter out = response.getWriter();
todoStream.forEach((Device device) -> {
String deviceString = JSON.toJSONString(device);
out.write(deviceString);
out.write("\n");
entityManager.detach(device);
});
out.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
第三步 :上述代码中的entityManager.detach(device) 负责删除缓存的对象,只有这样才能保证内存即用即销。另外EntityManager 需要注入。