概述
对于超大数据量的导出是一个长耗时的操作。在没有进度条情况下,当用户点击导出后,页面长时间得不到反馈,导致用户不清楚是导出出现问题,还是导出正在进行处理还未结束。所以,为超大数据量的导出添加进度条功能显得尤为必要。
下面以导出10000000手机号数据为例,开发带进度条的导出功能。
需求分析
- 进度条分析
文件的导出有两个阶段,一是查询数据库生成数据文件;二是数据文件下载。对于数据文件的下载各大浏览器都可以很好的显示下载进度,不需要添加进度条。所以,进度条展示的是一阶段查询数据库生成数据文件的进度,对于超大数据量生成文件的过程会持续很长的时间,此处使用进度条展示非常合适。
- 超大数据量导出查询数据分析
对于超大数据量的查询采用的基本方案为,先查询总数据量数,根据设置每次查询条数计算出需要查询的次数,然后分页查询将查询结果动态写入数据文件,在将数据文件以流的形式输出给浏览器。
传统方式导出开发
传统方式不使用进度条展示导出进度,导出效果如下:
- 页面代码如下:
<form action="download" method="post">
<input type="submit" value="导出">
</form>
- Java后端导出处理,代码如下:
其中导出文件暂存/tmp
目录下
public void download() throws IOException {
final String fileName = UUID.randomUUID().toString().replace("-","");
try (final FileWriter fileWriter = new FileWriter(new File("/tmp/" + fileName + ".csv"));
final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);){
final long totalNum = remoteGetUserNum();
final long pageSize = 100L ;
long totalPage = (totalNum%pageSize == 0L) ? totalNum/pageSize:(totalNum/pageSize + 1);
for (long pageNum = 1L; pageNum <= totalPage; pageNum++) {
List<String> userList = remoteGetUser(pageNum,pageSize);
for (String s : userList) {
bufferedWriter.write(s+"\r\n");
}
}
bufferedWriter.flush();
}
final HttpServletResponse response = getResponse();
final File file = new File("/tmp/" + fileName + ".csv");
response.addHeader("Content-Disposition","attachment;filename=" + new String(fileName.getBytes())+".csv");
response.addHeader("Content-Length",""+file.length());
response.setContentType("application/octet-stream");
try (final FileInputStream in = new FileInputStream(file);
final BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
final OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());){
byte[] buffer = new byte[1024];
int len;
while ((len = bufferedInputStream.read(buffer)) != -1){
outputStream.write(buffer,0,len);
}
outputStream.flush();
}
renderNull();
}
private List<String> remoteGetUser(long pageNum, long pageSize) {
return userList;
}
List<String> userList = new ArrayList<>();
{
for (long i = 0; i < 100; i++) {
userList.add(String.valueOf(18321714630L+i));
}
}
private long remoteGetUserNum() {
return 10000000L;
}
待进度条导出
效果如下:
- 进度条界面设计
在导出功能按钮下方编写静态进度条代码,通过控制内部div的宽度来实现进度的变化,默认隐藏。
<form action="download" method="post">
<input type="submit" value="导出">
<div id="progress" style="height:20px;width:100%;background: #efefef;border:1px solid #eee;border-radius:10px;display:none;">
<div class="bar" style="background: green;width:10%;height: 100%;border-radius:10px;line-height:20px;">0%</div>
</div>
</form>
静态效果如下:
- 前端页面下载请求改造
传统导出功能页面点击导出提交表单请求获取文件,使用进度条导出功能后,既要提交导出请求又要更新进度条变化,请求就变成了多任务。需要使用ajax请求来生成数据文件和更新进度条展示,等到后台数据文件生成好ajax请求得到处理完成的响应后,再通过js发起表单请求下载处理好的数据文件即可。
代码变更如下:
function download(){
var bar =$('#progress').show().find('.bar')
bar.text('0%')
bar.css({width:'0%'});
var taskId = '' + new Date().getTime()
var timer = setInterval(function(){
$.ajax({
type:'post',
url:'getProgress',
data:{taskId:taskId},
dataType: "json",
}).success(function(data){
if(data.result){
bar.text(data.value + '%')
bar.animate({width:data.value + '%'},300)
}
}).error(function(e){
})
},300);
$.ajax({
type:'post',
url:'download',
data:{taskId:taskId},
dataType: 'json',
}).success(function(data){
if(data.result){
jQuery('<form action="downloadFile" method="post">' + // action请求路径及推送方法
'<input type="text" name="taskId" value="' + taskId + '"/>' +
'</form>')
.appendTo('body').submit().remove();
}
clearInterval(timer)
$('#progress').hide().find('.bar').css({width:'0%'})
}).error(function(e){
clearInterval(timer)
$('#progress').hide().find('.bar').css({width:'0%'})
// do some things
})
}
- 后端java代码开发
将文件导出的两个阶段分开,download方法处理一阶段内容,分批查询生成数据文件,此阶段耗时较长需要添加进度控制;downloadFile方法处理二阶段内容,将数据文件以流的形式输出到浏览器,此阶段下载进度各大浏览器展示都很完善。
public void download() throws IOException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("result",true);
jsonObject.put("msg","成功");
final String taskId = getPara("taskId");
try (final FileWriter fileWriter = new FileWriter(new File("/tmp/" + taskId + ".csv"));
final BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);){
final long totalNum = remoteGetUserNum();
final long pageSize = 100L ;
long totalPage = (totalNum%pageSize == 0L) ? totalNum/pageSize:(totalNum/pageSize + 1);
for (long pageNum = 1L; pageNum <= totalPage; pageNum++) {
List<String> userList = remoteGetUser(pageNum,pageSize);
for (String s : userList) {
bufferedWriter.write(s+"\r\n");
}
taskProgressMap.put(taskId, Math.toIntExact(pageNum * 100 / totalPage));
}
}
renderJson(jsonObject.toJSONString());
}
public void downloadFile() throws IOException {
final String taskId = getPara("taskId");
final File file = new File("/tmp/" + taskId + ".csv");
final HttpServletResponse response = getResponse();
response.addHeader("Content-Disposition","attachment;filename=" + new String(file.getName().getBytes()));
response.addHeader("Content-Length",""+file.length());
response.setContentType("application/octet-stream");
try (final FileInputStream in = new FileInputStream(file);
final BufferedInputStream bufferedInputStream = new BufferedInputStream(in);
final OutputStream outputStream = new BufferedOutputStream(response.getOutputStream());){
byte[] buffer = new byte[1024];
int len;
while ((len = bufferedInputStream.read(buffer)) != -1){
outputStream.write(buffer,0,len);
}
outputStream.flush();
}
renderNull();
}
总结
- 超大数据导出需要分批查询处理。
- 超大数据导出一阶段(查询数据生成数据文件)耗时较长,需要添加进度监控。
本文只是提供一种简单的demo,进度条功能再其他耗时操作中都可以使用,应该做成js组件的形式供以后复用。
结尾附上我的开源消息组件地址(其中包含进度条组件:手把手教你开发一款属于自己的前端jQuery消息组件