大数据量报表系统的改进方案
2012年12月28日


如果是行家,提起报表,你是否想起了JasperReports( iReport )、Birt、JFreeReport、水晶报表等?但是,在海量数据问题面前,这些工具都弱爆了。怎么说呢,这些报表工具,的确功能强大、支持可视化的“报表模板”制作,用它来制作公文文件、财务报表、统计汇总表等,非常实用。但是我讲的是一种大数据量的报表,可能不是一张word或者几千条的excel能够导出的,比如说订单报表、销售明细报表,1天就是几万条,1个月几十万的数据,体现到报表文件(excel或者csv、txt等)就是几十Mb、上百Mb的文件大小,这个数量级的报表文件,excel或者记事本都是打不开的(文件太大,会直接卡死)。


我们系统(航空公司,包括国航、川航、海航等)的报表特点:

1、数据库的数据量是千万条的级别

2、导出的报表数据也是几万条、甚至上百万条数据的级别。

3、基于千万条数据量级别的数据计算(查找、排序、汇总、拼接等)


报表演进之路(一) 单独开设统计数据库

报表模块我们其实是专门开设的数据库,我们称之为“统计数据库”。也就是说,我们的报表程序没有连接其他数据库或webservice,统计中所有可能会用到的数据,都导入到统计数据库中去。利用Oracle的脚本编写JOB计划,可以定时在后台执行相关操作,例如每天晚上0点将一张表的数据保存到另一张表中,定时备份数据库等。


报表演进之路(二) 从xls文件到csv文本文件的转变

Excel 2003对xls文件的支持,最多是65536行,256列。显然6万多行的数据,是很容易突破的,我们的数据量很大。另外一个问题是,xls文件占用的存储空间比纯文本文件要大很多,因为它的每一格数据都包含了数据类型、精度等额外信息。而csv文件是纯文本文件,只包含原始文本信息,占用的空间小很多,和txt文件差不多。


报表演进之路(三) 引入锁机制,限制同时下载人数

因为报表下载线程占用的服务器资源很大,可能严重影响服务器的性能,而我们的系统主要功能并不是处理报表的,如果因为报表的处理而拖慢了整个系统,那么就非常不合理了。为了不影响到服务功能,我们为报表下载的程序加上了锁,同一时间最多只允许N个人下载,超过人数时会提示“对不起,当前的下载人数过多,请稍后再试……”。

同时,在服务器上也配置了最大会话时间、最大Servlet运行时间、最大线程连接数等相关参数,Tomcat服务器,可以在server.xml中配置,Websphere服务器,可以在httpd.conf中配置。某些参数还可以在项目的web.xml中指定,例如,可以在web.xml中做如下配置:

<servlet>
    <servlet-name>ReportDownload</servlet-name>
    <servlet-class>com.zollty. ReportDownload </servlet-class>
    <init-param>
         <param-name>timeout</param-name>
         <param-value>60000</param-value>
    </init-param>
</servlet>

该配置为单个Servlet指定了运行时限,这个Servlet线程一次最多只能运行60000ms,超过这个时间就会报Time out错误。


报表演进之路(四) 报表下载程序的优化

在下载报表时,由于种种原因,我们可能会感觉到“慢”。查询数据库需要时间、排序需要时间、转码需要时间、文件输入输出需要时间、网络数据传输需要时间……我们能做到的,就是将程序尽量的优化。为了达到最高的效率,我们处理报表的核心程序是一个Servlet,这个Servlet直接调用service层程序生成报表csv文件,然后将csv文件压缩成zip文件。我们的压缩强度是很高的,一个200多M的csv纯文本文件,压缩成zip文件后只有20多M。

同时,很关键的一点,我们没有用Hibernate等ORM工具。出于性能上的考虑,我们直接用的JDBC。

在程序性能优化上,有以下几点值得分享:

1. 程序执行流程的优化

算法,算法,还是算法。过多的跳转、过多的循环、过多的查询都是性能优化的重点,甚至过多的变量、过多的new关键字,都是需要注意的地方。Java程序本来就慢,更不能让糟糕的算法毁了它。举个例子,大家都知道:链表的插入操作比较快而查询操作比较慢。其实,这个快慢的差距是很微小的,但是大家都注意到了!然而,在实际开发中有很多差距几百毫秒、几千毫秒的地方,大家却视而不见。例如,我在接手报表开发时发现:原报表程序将数据库里面的数据查出来然后写入csv文件,然后再读取这个csv文件的内容转码成gbk另存为一个csv文件。我只能说这个算法太高级了,为什么不在第一次写入csv文件的时候就以gbk编码方式写入呢?

2. sql优化

关于Oracle的sql优化,还是有一定学问的,比如“查询条件的优化”、“索引的优化”这两条我认为是必须有的。还有一些高级的优化,可能涉及到Oracle执行计划什么的,对于某些数据量特别大的表,也是值得考虑的。sql优化举例如下:

1)多and条件按优先级排序,比如id这种能唯一确定一条数据的条件应放在前面

2)多表连接时,表的先后次序

3)大于和小于条件优化

4)用>=替代>

5)减少order by,ORDER BY的SQL语句会启动SQL引擎执行耗费资源的排序(SORT)功能。

6)为频繁查询的列和排序用到的列建立合适的索引。


3. 校验和错误处理的优化

例如,查询的时候,表单的数据是否合法,最好在页面上就用JavaScript校验避免将低级的无效条件提交给服务器端程序处理。还比如,防止表单的重复提交,防止频繁点击submit按钮等。错误日志的记录(一般是Log4j,有可能还会将错误信息存入数据库、发送邮件什么的),异常的处理(特别是有国际化功能的时候),都是值得注意的地方。这里有一个经验值得分享,那就是:操作等待。顾名思义,就是“当用户点击执行一个操作后,弹出一个覆盖层或者进度条,提示用户‘操作正在进行中,请稍等’,此时禁止用户再次点击或进行其他操作”。这种方法最好是配合ajax使用。


报表演进之路(五) 重新审视统计报表

从网站架构上看,报表模块我们专门开设了数据库,这样就足够了吗?为了不影响服务的性能,我们可以将系统的服务功能和报表功能分开。同时,从使用反馈来看,报表模块对他们来说是非常重要的,公司领导、财务部、市场部都很重视,他们迫切希望能够完善报表这一块,提高他们的工作效率、增强对数据和信息的分析能力。而且,由于系统同时支持多家航空公司,每个航空公司对报表的要求不尽相同,例如数据格式,数据内容等。为了不影响网站的售票,也为了扩展报表的功能,我们考虑将报表从原系统中剥离出来,单独做成一个项目,我们称之为报表引擎项目。

功能上讲,报表引擎支持及时下载、离线下载、定时下载;支持更丰富的统计,甚至包括图形统计;针对每个航空公司的可配置程度更高,报表数据格式,报表数据内容等都可以自定义。

技术上讲,将“统计汇总表”和“数据明细表”分开,“数据明细报表”做成csv文件格式,“统计汇总报表”在性能允许的范围内,可以做成xls、pdf、图形甚至flash。可以借鉴iReport、Birt等报表工具的源码,自己开发一套报表处理内核。离线下载主要是排队下载机制,没有Servlet线程运行时限和Session超时时限限制,可支持大数据量,报表生成后是放到服务器目录上,再提供一个url供用户去下载,报表生成后可以以邮件或短信方式提醒用户到指定地方去下载。