如何处理大数据量的查询

时间:2021-01-04 11:50:04
 在实际的任何一个系统中,查询都是必不可少的一个功能,而查询设计的好坏又影响到系统的响应时间和性能这两个关键指标,尤其是当数据量变得越来越大时,于是如何处理大数据量的查询成了每个系统架构设计时都必须面对的问题。本文将从数据及数据查询的特点分析出发,结合讨论现有各种解决方案的优缺点及其适用范围,来阐述J2EE平台下如何进行查询框架的设计。

Value List Handler模式及其局限性
在J2EE应用中,对于大数据量查询的处理有许多好的成功经验,比如Value List Handler设计模式就是其中非常经典的一个,见图1。该模式创建一个ValueListHandler对象来控制查询的执行以及结果集的缓存,它通过DAO(Data Access Object)来执行查询,并将数据库返回的结果集(传输对象Transfer Object的集合)缓存起来,接下来的客户端查询请求将直接从缓存中获得。它的特点主要体现在两点:服务器端缓存数据,每次只返回客户端本次操作所需的数据,通过这两个措施来减少数据库的访问次数以及增加客户端的响应速度,达到最优的查询效果。当然,这里面隐含一个前提就是客户端采用分页的方式来浏览数据。关于该模式的详细介绍,请参考[Core J2EE Patterns]一书。
  如何处理大数据量的查询 
图1:Value List Handler类图

但是在实际的应用过程中,会发现该模式存在一定的局限性,其实可以说是该模式应用具有一些前提条件:
1、由于缓存是以内存来换性能,这对于小数据量会工作得很好,但是如果结果集很大,内存消耗将会非常严重。同时,消耗在处理结果集上的时间也会越来越长,比如要循环读取记录集中的数据,然后依次填充每个传输对象,想想看几百万条数据这样处理起来肯定让人不能忍受。过长的处理时间不仅降低反应速度,同时还会占用宝贵的数据库连接资源,造成其它地方无连接可用。虽然,在DAO模式中利用CachedRowSet,Read Only RowSet ,RowSet Wrapper List等策略(详见参考资料)来代替Transfer Object Collection策略,有效地提高了处理速度,但是仍然存在着在大集合数据中进行定位、遍历等问题。试想一想,即使在CachedRowSet中的absolute(2000000)也是非常费时的操作。所有这一切的根源就在于缓存是一次性读取所有的数据,虽然有时你可以利用业务逻辑来强制性增加一些限制条件(比如产品查询必须选择大类和次类),但这种限制往往是不牢靠的或者说只是一时的权宜之计。也有人提出,可以不必缓存所有的查询结果,而采取只缓存部分结果集,比如500,1000条,但这样一来,就涉及到复杂的查询数据是否越界的控制,增加了复杂度,同时也不易实现。

2、既然使用缓存,那就不得不面对一个数据更新的问题,使用缓存,实际上就假定了在数据缓存期间,数据库中的数据不会改变,或者这些改变可以不被反映出来。但是,在很多场合下(比如常见的业务系统中)这些数据库中的数据经常会发生变化,而且这些改变需要及时反映给客户端。

3、缓存其实存在一个基本前提,就是缓存的数据会被客户端反复查询使用,具体到分页查询就是客户会选择不同的页数来查看数据。如果客户端的查询条件始终变化,或者用户基本上只关心第一页的数据(仔细琢磨一下用户的习惯,这在很多中应用场合都很常见),那缓存就失去了应有的意义,变得多此一举了。

数据分析
所以说,在决定是否应用某种设计模式前,我们需要对被查询数据的特点以及这些数据以何种方式被使用(查询的特点)进行一个分析,根据不同的结论来决定采用何种处理策略。而且,数据本身的特点和被使用的方式往往交织在一起,需要综合起来考虑,但这其中主要的考量点还是数据查询的特点。

一般来说,可以从以下几个方面来分析数据:
1、    数据量大。
这是我们今天讨论的数据的一个最基本特点,这个特点在查询框架设计时要引起足够的重视。
注意:大数据量的查询是指查询时匹配条件的数据量大,而不是指表中的数据量大,虽然大部分时候这两者都是一致的。因为在某些情况下,业务逻辑可以限制或者只需要一次获取很少量的数据,而查询的表中的数据量却可能很大,那这种情况就不属于本文的讨论范围。
2、    关联复杂,多表关联。
越是简单的数据可能关联越少,而越是复杂的数据往往都是多表关联,这样很多时候你需要将这几张表作为一个整体来考虑。
3、    变化频率。
从这个角度出发,可以大致将数据分为以下几类:几乎不变化的睡眠数据;有规律定时更新的数据,比如招聘网站的职位信息;经常性无规律更新的数据。
4、    成长性。
数据是否具有成长性,要预见数据的成长性,并在现有方案中考虑这种成长性,避免到时候查询框架的重新设计,象大部分的业务数据都具有这种成长性。
注意:这里也要特别注意区分数据本身的成长性和数据查询的成长性,这看似等同的两者其实还是存在很大的区别。就拿招聘网站来说,有效职位的数据肯定是一天天在增加,具有高成长性,但是在某个区间(比如一个月,一个星期)内的有效职位查询则变化不会太大,不具有成长性。而后者却往往是实际系统中最常遇到的查询情况。
5、    数据查询的频率和方式。
所有的数据查询不可能被等同地使用,你要分清楚系统中的几个关键查询,这些查询使用频率高,响应要快。试想一想,如果一个电子商务系统的产品查询每次都要让顾客等上十秒钟,结果就可想而知。

用户的使用习惯分析
除了对数据查询本身需要进行分析之外,我们还需要去分析一下用户如何来使用或者看待这些数据,用户的使用习惯如何。有人可能觉得这作用不大,或者很难去分析,其实查询的最终使用者是用户,他们的一些习惯会很大程度上左右你的设计。

1、    用户关心数据哪些方面的特性,不关心哪些方面的特性。
上面我们分析了数据本身的许多特性,那用户对其中哪些特性最敏感呢?比如说对脏数据特别不能接受,那我们就必须在查询框架设计时特别照顾到这一点。因为再好的框架设计都不可能在每个方面都能达到最优的效果,当必须有所取舍的时候,我们就要明白哪些特性是客户最关心的。

2、    用户如何来使用数据。
现在一般查询的客户端都采用分页的方式,一个查询可能会存在十几页甚至几十页结果。对于某些查询,用户可能往往只关心第一页或者前几页的结果,比如用户需要查询出最近完成的工单,而对于另外一些查询,用户可能对所有页结果都很关注,比如用户查询出最近三天新增的招聘职位。这不同类型的查询在查询框架设计的时候都需要有所考虑并给予不同的处理策略。

查询框架的设计
对数据及用户使用习惯进行了仔细的分析,接下来就可以根据这些分析来设计你的查询框架了。在J2EE架构下,对于大数据量的查询主要采取以下两种方法:
基于缓存的方式:
从数据库得到全部(部分)数据,并将其在服务器端进行缓存,接下来的客户端请求,将直接从缓存中取得需要的数据。这其实就是Value List Handler模式的原理,它主要适用于数据量不是非常大,变化不是很频繁(或者变化频繁但是有规律)且不具有成长性的情况,比如招聘网站或者电子商务网站的大部分查询就非常适合采取这种方式。

采用这种方式,要特别注意第一次查询问题,避免响应性能达不到要求,因为每个查询第一次都需要连接数据库,从中获取数据并缓存起来,所以第一次查询会比接下来的查询都显得更慢一些。

对于数据的缓存,有以下几种实现方式:
    直接缓存在服务器端
Value List Handler模式就采取这种方式,并且可以根据不同的情况采取不同的缓存策略,比如Transfer Object集合,CachedRowSet等,这取决于你的DAO实现策略。
    用临时表来保存查询结果
WLDJ(www.sys-con.com/weblogic/)杂志2004年第7期上有一篇名为“Handling Large Database Result Sets”的文章,它详细介绍了如何利用临时表来改良Value List Handler模式以支持大型的J2EE应用。

当然除了以上这些方法以外,实现缓存也可以求助于操作系统的特定实现,以前我在IBM DW发表过一篇探讨MMF在Java中应用的文章(见参考资料),可惜未有深入,有兴趣的朋友可以参考一下。

在使用Value List Handler模式时,要特别注意以下几点:
1、    该模式一般和DAO模式搭配使用。
2、    该模式有POJO,stateful session bean两种实现策略。
3、    如果采取stateful session bean实现策略,则默认该缓存的时间长度为整个用户会话。

前面我们也提到过,如果数据不是绝对不变的,那缓存就面临更新的问题,一旦更新就可能存在着数据不一致,如果恰巧客户也希望能够看到变化的效果,这个时候就需要采取某种措施来保证这种一致性。常见的措施可以是设置一个标志位,每次发生数据更新后都将其对应的标志位更新,查询时如果发现标志位更新了,就直接从数据库获取数据,而不是从缓存中获取数据。另外一种方式就是数据更新的同时主动去清空session中的缓存,如果采用stateful session bean实现策略的话。

当然,采取缓存方式的大数据量查询一般来说都不大可能遇到设置更新标志位的问题,因为这种应用方式决定了数据不大可能变化,或者数据变化不要求立刻反应给用户。比如招聘网站新增加了一些职位信息,如果这些更新恰巧发生在某些用户的会话期间,且没有设置更新标志位,那这些新增信息就不会反应到用户的查询结果中,这种处理方式也是可以接受的。

基于查询的方式:
不进行数据缓存,客户端的每次数据请求都需要进行实际的数据库查询,这种方式适用于量大,具有成长性,变化频繁的数据。该方式的特点是每次查询的时间都大致相等,不会存在基于缓存的方式的第一次查询问题,但后续的操作会比缓存方式的查询慢一些。采取这种方式的查询框架设计更具有可扩展性以及对数据变化更好的应变能力,在大部分的业务系统中都推荐使用该方式。

使用这种方法,每次查询应该只从数据库获得客户端所需的数据,这样就涉及到如何获得部分数据的问题。一种是查询出符合条件的所有记录,然后遍历该记录集根据上次查询结果来比较记录中的某些字段获取本次查询需要的部分数据,由于要对记录集进行遍历,效率不高,一般都不推荐使用,而往往采用另一种增加sql查询语句条件的方式,这种方式有以下几种实现策略:
    专属于数据库的,比如Oracle的rownum
有些数据库提供了标识查询结果集中行号的功能,利用该标识就可以限定某个范围的记录,比如下面这个方法就是利用Oracle数据库中的rownum功能来包装sql查询语句以获得部分记录集。
private String wrapSQL(String strSql) {
      String strWrapSql = "";
      strWrapSql = "select * from (select rownum mynum, xxx.* from (" + strSql + ") xxx ) where mynum between " + getFrom() + " and " + getTo();
      return strWrapSql;
}

当然这种实现策略的缺点很明显,就是绑定于特定数据库的实现,有的数据库可能并不提供这个功能,不能独立于特定数据库实现而提供一个通用的实现。
    利用业务数据的规律性来获得部分记录集
对于实际系统中的每个查询,都会被指定按照某种方式来进行排序(不管是业务逻辑默认的还是客户端指定的),也就是说查询的sql中存在着order by子句。既然查询结果集对于order by子句中的字段会呈现一定的规律性,那我们是不是可以利用这种有序性来获得部分的记录集呢?答案是肯定的。

参考资料中的Paging in J2EE一文对这种方式进行了详细的阐述,并且给出了一个实例,有兴趣的朋友可以参考一下。但是,该篇文章针对的是那些排序方式是业务逻辑默认的情况,即排序字段可以通过属性文件定义下来的。但在实际情况中,还存在另外一种可能,排序方式是由客户端指定时,这个时候排序的字段和升降序都由客户端指定,这个时候就需要在查询框架中提供某种机制来将排序字段和where子句进行映射。

获得结果集的总数
当采用获取部分记录集的方式时,获得的记录集大小并不能真实地反映出查询结果集的大小(因为只是其中的一部分),所以又涉及到对于记录总数的处理:
    如果不需要,或者没有意义,不提供,因为count(*)操作耗资源
    在sql语句中获得
在有些情况下,你可以在sql语句中直接加上count(*)来获得记录集总数,比如你用Statement接口的setMaxRows(int max)方法来控制返回的记录数时。
    单独进行一次查询
为count(*)语句单独进行一次数据库查询,只要将构造好的sql语句中的select字段替换为count(*)即可。当然count(*)的查询sql应该尽量简练,不要加上order by子句之类的。

其实,以上提到的这两种方式在实际的查询框架设计时不可能完全分开来,只不过以哪种方式为主而已,它们往往是混合在一起使用以达到最优的查询效果。

查询框架设计时的注意点
当然,在优化大数据量查询的过程中,数据库本身的优化必不可少,比如建立索引等措施。但是,数据库的优化要循序渐进,要和程序代码的优化相一致。在进行数据库优化之前,要做的是sql语句的优化,这可以通过观察它的执行计划来进行考量。这些工作最好都由DBA来参与共同完成。

对于性能的个人态度:要提早考虑,确定一个方案前要进行测试,并预见未来的变化。

对于“大数据量”这几个词不要只停留于口头的重视,要想有切身的体会,最好在框架设计好后,进行大数据量的模拟压力测试,实际地检验你的设计。不要总是回答:我想,应该没有问题,应该支持几百万的数据,最好拿出最有说服力的数据。

以后关注的方向:
1、    EJB 3.0规范中EJB QL语法得到加强,比如可以支持原生SQL语句,使得利用实体Bean来实现部分类型查询成为可能。
2、    JDBC规范是否能够在某些方面对这种大数据量查询提供某些功能接口,比如获得部分记录集,以及统一的缓存接口。

总结

本文给出了查询框架设计时的一些思路和方法,最重要的一点就是在设计之前要充分研究你的目标系统,了解最终的数据查询特点。由于本文只是个人的一些观点和经验总结,偏颇之处在所难免,希望有兴趣的朋友一起来讨论这个问题。

参考资料:
1、    [Core J2EE Patterns] Core J2EE Patterns: Best Practices and Design Strategies, Second Edition, By Deepak Alur, John Crupi, Dan Malks.
2、    关于DAO模式中CachedRowSet等策略的详细讨论,请参考[Core J2EE Patterns]一书。
3、    Paging in J2EE: Manage Large Result Sets Efficiently. By Lara D'Abreo.
( http://www.devx.com/Java/Article/21383)
4、    如何提高系统性能指标??MMF在Java中的应用
( http://www-900.ibm.com/developerworks/cn/java/l-java-performance/index.shtml)