乐趣区

关于java:从系统报表页面导出20w条数据到本地只用了4秒我是如何做到的

背景

最近有个学弟找到我,跟我形容了以下场景:

他们公司外部管理系统上有很多报表,报表数据都有分页显示,浏览的时候速度还能够。然而每个报表在导出工夫窗口略微大一点的数据时,就异样迟缓,有时候多人一起导出时还会呈现堆溢出。

他晓得是因为数据全副加载到 jvm 内存导致的堆溢出。所以只能对工夫窗口做了限度。以防止因导出过数据过大而引起的堆溢出。最终拍脑袋定下个限度为:导出的数据工夫窗口不能超过 1 个月。

尽管问题解决了,然而经营小姐姐不开心了,跑过来和学弟说,我要导出一年的数据,难道要我导出 12 次再手工合并起来吗。学弟心想,这也是。零碎是为人服务的,不能为了解决问题而扭转其本质。

所以他想问我的问题是:有没有什么方法能够从根本上解决这个问题。

所谓从根本上解决这个问题,他提出要达成 2 个条件

  • 比拟快的导出速度
  • 多人能并行下载数据集较大的数据

我听完他的问题后,我想,他的这个问题预计很多其余童鞋在做 web 页导出数据的时候也必定碰到过。很多人为了放弃零碎的稳定性,个别在导出数据时都对导出条数或者工夫窗口作了限度。但需求方必定更心愿一次性导出任意条件的数据集。

鱼和熊掌是否兼得?

答案是能够的。

我动摇的和学弟说,大略 7 年前我做过一个下载核心的计划,20w 数据的导出大略 4 秒吧。。。反对多人同时在线导出。。。

学弟听完表情有些兴奋,然而眉头又一皱,说,能有这么快,20w 数据 4 秒?

为了给他做例子,我翻出了 7 年前的代码。。。花了一个早晨把外围代码抽出来,剥离洁净,做成了一个下载核心的例子

超快下载计划演示

先不谈技术,先看成果,(残缺案例代码文末提供)

数据库为 mysql(实践上此套计划反对任何结构化数据库),筹备一张测试表t_person。表构造如下:

CREATE TABLE `t_person` (`id` bigint(20) NOT NULL auto_increment,
  `name` varchar(20) default NULL,
  `age` int(11) default NULL,
  `address` varchar(50) default NULL,
  `mobile` varchar(20) default NULL,
  `email` varchar(50) default NULL,
  `company` varchar(50) default NULL,
  `title` varchar(50) default NULL,
  `create_time` datetime default NULL,
  PRIMARY KEY  (`id`)
);

一共 9 个字段。咱们先创立测试数据。

案例代码提供了一个简略的页面,点以下按钮一次性能够创立 5w 条测试数据:

这里我间断点了 4 下,很快就生成了 20w 条数据,这里为了展现下数据的大抵样子,我间接跳转到了最初一页

而后点开 下载大容量文件 ,点击执行执行按钮,开始下载t_person 这张表里的全副数据

点击执行按钮之后,点下方刷新按钮,能够看到一条异步下载记录,状态是 P,示意pending 状态,不停刷新刷新按钮,大略几秒后,这一条记录就变成 S 状态了,示意Success

而后你就能够下载到本地,文件大小大略 31M 左右

看到这里,很多童鞋要纳闷了,这下载下来是 csv?csv 其实是文本文件,用 excel 关上会失落格局和精度。这解决不了问题啊,咱们要 excel 格局啊!!

其实略微会一点 excel 技巧的童鞋,能够利用 excel 导入数据这个性能,数据 -> 导入数据,依据提醒一步步,当中只有抉择逗号分隔就能够了,要害列能够定义格局,10 秒就能实现数据的导入

你只有通知经营小姐姐,依据这个步骤来实现 excel 的导入就能够了。而且下载过的文件,还能够重复下。

是不是从实质上解决了下载大容量数据集的问题?

原理和外围代码

学弟听到这里,很兴奋的说,这套计划能解决我这里的痛点。快和我说说原理。

其实这套计划外围很简略,只源于一个知识点,活用 JdbcTemplate 的这个接口:

@Override
public void query(String sql, @Nullable Object[] args, RowCallbackHandler rch) throws DataAccessException {query(sql, newArgPreparedStatementSetter(args), rch);
}

sql 就是 select * from t_personRowCallbackHandler 这个回调接口是指每一条数据遍历后要执行的回调函数。当初贴出我本人的 RowCallbackHandler 的实现

private class CsvRowCallbackHandler implements RowCallbackHandler{

    private PrintWriter pw;

    public CsvRowCallbackHandler(PrintWriter pw){this.pw = pw;}

    public void processRow(ResultSet rs) throws SQLException {if (rs.isFirst()){rs.setFetchSize(500);
            for (int i = 0; i < rs.getMetaData().getColumnCount(); i++){if (i == rs.getMetaData().getColumnCount() - 1){this.writeToFile(pw, rs.getMetaData().getColumnName(i+1), true);
                }else{this.writeToFile(pw, rs.getMetaData().getColumnName(i+1), false);
                }
            }
        }else{for (int i = 0; i < rs.getMetaData().getColumnCount(); i++){if (i == rs.getMetaData().getColumnCount() - 1){this.writeToFile(pw, rs.getObject(i+1), true);
                }else{this.writeToFile(pw, rs.getObject(i+1), false);
                }
            }
        }
        pw.println();}

    private void writeToFile(PrintWriter pw, Object valueObj, boolean isLineEnd){...}
}

这个 CsvRowCallbackHandler 做的事就是每次从数据库取出 500 条,而后写入服务器上的本地文件中,这样,无论你这条 sql 查出来是 20w 条还是 100w 条,内存实践上只占用 500 条数据的存储空间。等文件写完了,咱们要做的,只是从服务器把这个生成好的文件 download 到本地就能够了。

因为内存中一直刷新的只有 500 条数据的容量,所以,即使多线程下载的环境下。内存也不会因而而溢出。这样,完满解决了多人下载的场景。

当然,太多并行下载尽管不会对内存造成溢出,然而会大量占用 IO 资源。为此,咱们还是要管制下多线程并行的数量,能够用线程池来提交作业

ExecutorService threadPool = Executors.newFixedThreadPool(5);

threadPool.submit(new Thread(){
    @Override
    public void run() {下载大数据集代码}
}
                

最初测试了下 50w 这样子的 person 数据的下载,大略耗时 9 秒,100w 的 person 数据,耗时 19 秒。这样子的下载效率,应该能够满足大部分公司的报表导出需要吧。

最初

学弟拿到我的示例代码后,通过一个礼拜的批改后,上线了页面导出的新版本,所有的报表提交异步作业,大家对立到下载核心去进行查看和下载文件。完满的解决了之前的 2 个痛点。

但最初学弟还有个疑难,为什么不能够间接生成 excel 呢。也就是说在在 RowCallbackHandler 中继续往 excel 里写入数据呢?

我的答复是:

1. 文本文件流写入比拟快

2.excel 文件格式如同不反对流继续写入,反正我是没有试胜利过。

我把剥离进去的案例整顿了下,无偿提供给大家,心愿帮忙到碰到相似场景的童鞋们。

关注作者

关注公众号 「元人部落」 回复”导出案例“取得以上残缺的案例代码,间接能够运行起来,页面上输出 http://127.0.0.1:8080 就能够关上文中案例的模仿页面。

一个只做原创的技术科技分享号

退出移动版