web应用开发(尤其是xxx管理系统)经常遇到这样的需求:将当前列表中的数据导出到excel。往往这种需求很容易实现,但是要是导出的数据量特别大呢?本文介绍一种简单的大量数据导出的实现方案。
效果展示

如何解决大量数据导出内存oom的问题?
使用SXSSFWorkbook解决excel导出大量数据时占用内存大的问题。关键原理是:Streaming version of XSSFWorkbook implementing the “BigGridDemo” strategy. This allows to write very large files without running out of memory as only a configurable portion of the rows are kept in memory at any one time. 另外,服务端可以使用分页式从数据库中查询要导出的数据,而不是一次查询所有要导出的数据,节省内存开销。注意,这时候会不会有可重复读,幻读的问题呢?mysql默认支持的事务级别是可重复读,因此可以解决分页式多批次查询数据的不一致问题,而幻读的问题还是可能存在,但是在导出的应用场景中,往往是导出某一个条件区间的数据(比如过去某一时间区间),因此并发的区间插入导致的幻读问题发生概率较小。 但是分页查询相比一批次查询全部数据性能上会慢很多,分页查询时经常用到的limit语句,及时只查询最后一页的数据但是还是会扫描前面页的数据。 关键代码(使用easypoi):
Workbook workbook = ExcelExportUtil.exportBigExcel(new ExportParams(), Response.class,
new ExcelExportServer(), condition);
workbook.write(response.getOutputStream());
增加进度条功能
一方面防止请求超时,一方面增加了用户体验。
并发控制
增加并发导出席位控制,因为导出大量数据到excel,比较耗cpu,io。
实现
以下代码片段实现了全局的并发席位控制,并且提供了对导出任务的管理: 1、开始导出时,通过调用addExportServer方法将一个导出任务加入到并发席位。 2、导出完成时,通过removeExportServer方法从并发席位移除。 3、导出过程中,可以通过getProgress获取当前session下所有导出任务的导出进度。
public class ExportManager {
// 导出并发槽位
private final int capacity;
private final List<ExportMonitor> slots = new ArrayList<ExportMonitor>();
private final static int MAX_CAPACITY = 5;
private final static Logger logger = LoggerFactory.getLogger(ExportManager.class);
public ExportManager(int capacity) {
this.capacity = capacity;
}
/**
* 把导出server添加到并发槽位
*
* @param exportServer
* @return 如果并发槽位已满,返回false;如果重复添加也返回false;否则加入到并发槽位
*/
public synchronized boolean addExportServer(ExportMonitor exportServer) {
if (slots.size() >= capacity) {
return false;
}
for (ExportMonitor server : slots) {
if (server == exportServer) {
return false;
}
}
slots.add(exportServer);
logger.info("[并发槽位+] " + this.toString());
return true;
}
/**
* 导出完成时移除导出监控管理类
*
* @param exportServer
*/
public synchronized void removeExportServer(ExportMonitor exportServer) {
slots.removeAll(Lists.newArrayList(exportServer));
logger.info("[并发槽位-] " + this.toString());
}
/**
* 获取当前session下的所有的导出任务的导出进度
*
* @return
*/
public List<ExportProgressVo> getProgress() {
List<ExportProgressVo> list = Lists.newArrayList();
String sessionId = LoginUtils.getSessionId();
for (ExportMonitor server : slots) {
// 返回同一个session的所有的export server
if (Objects.equal(server.getSessionId(), sessionId)) {
list.add(server.getProgress());
}
}
return list;
}
@Override
public String toString() {
return "ExportManager [capacity=" + capacity + ", slots=" + slots + "]";
}
// 单例
private static class SingletonHolder {
private final static ExportManager instance = new ExportManager(MAX_CAPACITY);
}
public static ExportManager getSingletonInstance() {
return SingletonHolder.instance;
}
导出的clint需要实现ExportMonitor接口,以便自己被ExportManager管理。
/**
* 导出大量数据到excel的过程监控
*
* @author dinglingxiang
*
*/
interface ExportMonitor {
/**
* 获取当前导出进度
*
* @return
*/
ExportProgressVo getProgress();
/**
* 取消导出
*/
void cancel();
/**
* 获取sessionId
*
* @return
*/
String getSessionId();
}
使用示例
以下通用基类实现了ExportMonitor接口(用户监控和查询进度),和IExcelExportServer(easypoi接口用于分页导出)。 具体的导出任务只需要继承这个基类并给pageQueryFunction函数赋值。
// 分页查询函数
private Function<PageQueryCond<xxQueryCond>, PageInfo<xxResponse>> pushFunction = cond -> xxService
.getXxByCondition(cond);
class XxExcelExportServer extends ExcelExportServerBase<xxQueryCond, xxResponse> {
public XxExcelExportServer(
Function<PageQueryCond<xxQueryCond>, PageInfo<xxResponse>> pageQueryFunction) {
super(pageQueryFunction, "xx导出");
}
}
/**
* export server
*
* @author dinglingxiang
*
*/
public class ExcelExportServerBase<Q, R> implements ExportMonitor, IExcelExportServer {
// 当前导出行
private long currentRowNum;
// 总共数据行数
private long total;
// 开始导出时间
private Date startTime;
// session id
private final String sessionId;
// 下载任务描述
private final String description;
// 分页查询函数
private final Function<PageQueryCond<Q>, PageInfo<R>> pageQueryFunction;
public ExcelExportServerBase(Function<PageQueryCond<Q>, PageInfo<R>> pageQueryFunction, String description) {
this.pageQueryFunction = pageQueryFunction;
this.startTime = new Date();
this.sessionId = LoginUtils.getSessionId();
this.description = description;
// 开始下载,添加到并发槽位中
if (false == ExportManager.getSingletonInstance().addExportServer(this)) {
throw new RuntimeException("导出任务并发已满,请稍后再试");
}
}
/**
* 获取sessionId
*
* @return
*/
@Override
public String getSessionId() {
return sessionId;
}
@Override
public List<Object> selectListForExcelExport(Object queryParams, int page) {
@SuppressWarnings("unchecked")
PageQueryCond<Q> cond = (PageQueryCond<Q>) queryParams;
PageCond pageCond = cond.getPageCond();
pageCond.setPage(page);
pageCond.setSize(1000);
PageInfo<R> res = pageQueryFunction.apply(cond);
this.total = res.getTotal();
this.currentRowNum = res.getEndRow();
List<Object> list = Lists.newArrayList();
res.getList().forEach((o) -> list.add(o));
// 数据已经完全导出,释放并发槽位
if (CollectionUtils.isEmpty(list)) {
ExportManager.getSingletonInstance().removeExportServer(this);
}
return list;
}
@Override
public ExportProgressVo getProgress() {
ExportProgressVo progress = new ExportProgressVo();
progress.setCurrent(this.currentRowNum);
progress.setTotal(this.total);
progress.setTimeUsed(DateUtils.secondsBetween(startTime, new Date()));
progress.setDescription(this.description);
return progress;
}
@Override
public void cancel() {
// do nothing
}
@Override
public String toString() {
return "ExcelExportServerBase [startTime=" + startTime + ", sessionId=" + sessionId + ", description="
+ description + "]";
}
前端进度条展示
前端基于angularJS和bootstrap实现了进度条展示功能。
<!--导出进度条-->
<div class="loading-mask" ng-if="loading != null">
<div class="loading-content">
<div data-ng-repeat="x in exportTasks">
<div class="progress-span">
<span >{ {x.description}} 已用时{ {x.timeUsed}}秒 </span>
</div>
<div ng-class="{progress: true, 'progress-striped': x.striped}">
<div ng-class="['progress-bar', 'progress-bar-info']" ng-style="{width: x.percent + '%'}" style="transition: width 1s ease;">
<div >{ {x.percent}}%</div>
</div>
</div>
</div>
</div>
</div>
通过定时器查询导出进度:
// 开始loading
$scope.startLoading = function() {
$scope.loading = $interval(function () {
$scope.getExportProgress();
}, 1000);
};
// 结束loading
$scope.endLoading = function() {
//Cancel the Timer.
if (angular.isDefined($scope.loading)) {
$interval.cancel($scope.loading);
}
$scope.loading = null;
};
// 查询服务端获取导出进度
$scope.getExportProgress = function() {
$http.get(ApiConfig.baseUrl + "/v1/export/progress").then(function (result) {
$scope.exportTasks = result.data.content;
$scope.exportTasks.forEach(o => o.percent = parseInt(o.current * 100 /o.total));
});
};