实现报表的可控缓存
【摘要】
使用缓存可以提升报表性能是不争的事实,一般高端报表工具都会提供报表缓存功能,可将整个报表计算结果缓存在文件系统中,以便用户下次访问相同参数的报表时可以快速读取缓存结果进行展现。但有些情况下报表开发人员还希望对缓存的内容进行更准确和灵活的控制,比如缓存的不是整个报表结果而是其中一部分、缓存内容可被其它报表或程序复用,以及对不同的缓存结果设置不同的超时时间,从而应对数据量和实时性方面的不同情况。这时,一般的报表缓存就无法满足需求了。
怎么办?看这里:实现报表的可控缓存
集算器与报表结合使用时,可以帮助开发人员灵活控制缓存内容。这里我们将开发人员在使用集算器可以灵活控制的报表缓存内容称为可控缓存。可控缓存可以带来更大的灵活性和好处,充分解决实际应用中的报表性能问题。下面我们就对前面提到的部分缓存、缓存复用和设置不同超时时间三个方面展开讨论。
部分缓存
在报表开发中,有时并不希望将所有报表结果进行缓存,这样可以避免耗费过高的缓存成本(磁盘空间和应用服务器资源开销)。另外,当报表中的部分数据实时性要求很高,需要实时与数据库交互进行数据查询,那么这部分数据也不适合进行缓存。
通过集算器的可控缓存可以将变化不频繁的中间结果缓存起来,当报表再次请求时,实时性要求高的数据仍然实时从数据库中读取,同时结合缓存中的非实时数据进行报表计算,得到最终报表结果集。
常规缓存方案没有这种缓存部分结果的功能,只能设置整个报表是否进行缓存,这样报表在涉及不同时效性数据时就会发生矛盾,而集算器实现的可控缓存显然更加灵活,效率更高。
举例
订单(Orders)数据中超过 3 个月的数据就不再发生变化(冷数据)
数据结构如下:
现查询近 6 个月的订单明细并汇总月订单数量和金额。
报表表样如下:
直接从数据库订单表检索 6 个月的数据实现虽然最简单,但每次查询都要从数据库取全部数据显然性能不高。可以将 3 个月以上不变的数据在初次查询时缓存起来,而 3 个月内的数据仍然从数据库读取,这样以后再查时直接读缓存和数据库数据来加速报表性能,可以显著减少数据库的计算时间和 JDBC 的传输时间。
集算器将根据查询月份判断:
- 如果查询的是历史数据第一次查询数据库并写缓存,以后直接读缓存;
- 如果查询的是实时数据则每次都从数据库读取;
- 如果查询的数据包含历史数据和实时数据则将历史数据写缓存,实时数据读取数据库。
集算器实现
参数
查询参数为起止月份(每个月 1 日),实现中日期过滤数据均通过起止参数处理
集算器数据准备
A | B | C | |
---|---|---|---|
1 | =filePath=”/usr/report/cache/” | / 缓存目录 | |
2 | =reportName=”orders_customer_month” | / 报表名称,缓存使用报表名 + 参数命名 | |
3 | =elapse@m(pdate@m(now()),-3) | / 三个月前日期 | |
4 | =his_end=[end,A3].min() | / 历史数据日期终值 | |
5 | =pdate@me(end) | / 查询日期终值 | |
6 | =sql=”select 公司名称 客户, 订单 ID, 订购日期, 订单金额 from 订单, 客户 where 订单. 客户 ID= 客户. 客户 ID and 订购日期 >=? and 订购日期 <=?” | ||
7 | =f=file(filePath/reportName/”=”/begin/”+”/his_end) | / 缓存文件 | |
8 | =rs=[] | / 报表结果集 | |
9 | if f.exists() | // 如果有缓存 | |
10 | =f.import@b() | / 历史数据读缓存 | |
11 | >rs=rs|B10 |
/ 缓存结果添加到结果集 | |
12 | =elapse@m(A3,1) | / 查询数据库起始日期 | |
13 | if B12<end | / 有实时数据查询数据库 | |
14 | =connect(“demo”) | ||
15 | >rs=rs|C14.query@x(sql,B12,A5) |
||
16 | return rs | ||
17 | else | // 无缓存 | |
18 | =connect(“demo”) | ||
19 | =rs=B18.query@x(sql,begin,A5) | / 全量数据读库 | |
20 | if begin<his_end | / 将历史数据写入缓存 | |
21 | =B19.select(订购日期 >=begin && 订购日期 <=A5) | ||
22 | >f.export@b(C21) | ||
23 | return rs |
脚本解析:
1、A1-A2 分别设置缓存目录和报表名称,报表名称用于缓存文件命名
2、A3-A5 根据月份参数计算历史数据日期、实时数据日期等
3、A6 为查询 SQL,由于后面会重复使用,这里将其赋值给 sql 变量
4、A7 设置缓存文件,文件名为:报表名 = 缓存起始月 - 缓存终止月,如:orders_customer_month=2014-01-01+2014-04-01
5、A8 定义报表数据集变量 rs,后续读缓存和查询数据库结果都会追加到 rs 中
6、A9-C16 判断缓存(包含三个月以上数据)如果存在,则根据查询月份读取缓存历史数据(B10),如果还包含实时数据则查询数据库(C15),结果集追加到 rs 中并为报表输出结果(B16)
7、A17-C23 如果没有缓存(可能是初次查询,也可能查询的是实时数据),则直接查询数据库并返回结果(B19),若查询数据中包含历史历史数据,则写缓存(C21 和 C22)
报表调用
这里假定读者已经了解集算器与报表的关系,集算器仅为报表提供数据准备,将计算结果以数据集的方式提供给报表进行呈现。集算器脚本可以被润乾报表 5.0 及以上版本直接引用(集算器数据集);如果是其他报表工具,集算器提供了标准 JDBC 和 ODBC 接口,可以采用类似调用存储过程的方式调用集算器脚本,详细可以参考教程《应用集成 - 被 JAVA 调用》章节,以及《集算器与 BIRT 集成》或《集算器与 JasperReport 集成》。
以上通过举例说明了通过集算器实现报表缓存部分结果提升报表性能的过程,这种方式可以灵活控制缓存内容,加速报表运行。此外,将结果缓存到磁盘避免了通过数据库 JDBC 取数效率低下的问题,数据量大时尤其适用,同时由于缓存无需和数据库交互,降低了数据库的访问和计算压力,在报表加速的同时缓解了数据库负担。
值得注意的是,业务数据无论是否分库都可以使用这个方式提升报表性能。
缓存复用
集算器实现的可控缓存可以复用,一个报表的缓存结果(部分或全部)可以被其他报表或程序读取并使用,而不必像常规报表缓存方案那样重复缓存同样的结果,这同样也会大幅度提高整体缓存的效率。
与缓存部分结果适应实时性要求的情况类似,当其他报表或程序使用某个报表的缓存结果时,只需从缓存中(一般是磁盘文件)读取,并与报表中其他数据来源(可能是 DB、文件,或是另一个报表的缓存)进行混合运算,就能得到报表需要的结果集。而常规的报表缓存以报表模板为单位进行缓存,彼此无法复用,在造成资源浪费之外还会增加一定的性能开销。
上例中,如果另一张报表希望按客户来汇总订单情况:
报表表样如下:
熟悉报表的开发者都知道,这两张报表只是分组方式不同,数据源是完全一样的,这样我们就还可以使用上面的代码获取数据源,并且生成和访问缓存,这两张报表就可以共用缓存了。
设置不同的超时时间
我们都知道缓存一定都会有超时时间,超过时间后的缓存就会因为失效而被清除,报表再访问时需要重新生成缓存文件。
一般报表工具的缓存超时时间会在配置文件中设置,如 3600s 或 7200s,这种设置有时作用于单张报表的所有参数,有时甚至作用于所有报表,换句话说,整个报表甚至整个系统必须使用同样的设置。
显然,这种做法的性能并不高,很难兼顾不同更新频率的数据。如果能够针对不同的报表场景设置不同的超时时间,那样会更加有效。例如,针对大量历史数据进行查询的报表,由于历史数据的变化不大,我们希望报表的缓存结果可以保存较长时间,以便每次查询时都能从缓存中快速读取结果;而针对数据变化频繁,实时性要求较高的报表则希望超时时间较短,以便充分满足数据的实时性要求。
集算器实现的可控缓存允许开发人员针对不同的报表需求设置不同的超时时间,以应对上述提到的报表场景,例如可以在第一个例子中增加超时设置。这种做法提供了更高的灵活性,使得报表缓存达到真正意义上的精确可控。
举例
沿用第一个例子,我们增加相应的超时设置。由于该报表月末查询比较频繁,因此希望缓存有效时间长一些(7 天)。这时只需要将 A9 的表达式改为:
A9: if f.exists()&& interval(f.date(),now())<=7
判断缓存文件如果存在,并且缓存日期为 7 天内,则读取缓存。
本文的例子中用到的是单数据集报表,而实际上多数据集报表需要可控缓存的情况更多。一个报表的多个数据集很可能变化的频率相差很大,有的数据集很稳定,几天甚至几个月都不会变,而有的数据集则可能随时都在变化。采用部分缓存并设置不同的超时时间,这时对缓存的可用性就非常有意义了,能够在确保报表数据正确性的同时充分利用缓存手段提高访问性能。
需要说明的是,集算器实现可控缓存也有其适用场景,并不能完全取代常规缓存,常规缓存手段会连同报表计算结果以及呈现属性保存在一起,而这里的可控缓存只缓存数据,在呈现时还要再次进行外观计算,因此更适用于数据计算强度较高,但外观计算强度较低的场景。在实际应用中,可以取长补短,将两者结合起来使用。