当我们为了完成某个业务逻辑,需要从数据库系统返回记录过多的结果集的时候,往往会引发多方面的问题,这些问题包括:性能优化设计策略、结果集的分页技术等等。对于开发人员来说,都是无法避免的。
先看看数据库查询大结果集可能带来的问题:
(1),内存占有过大。如果我们一次性把成千上万条记录保存在内存的话,内存势必会消耗过大,这使得JVM虚拟机能运用的内存空间减少,数据交换的速度减低,导致服务性能明显降低;而且,很容易引发内存不足不可恢复异常,最后导致整个服务瘫痪。
(2),网络传输阻塞。一般情况下,数据库服务都是独立一台机器,如果我们一次性从数据库返回大量数据的话,很容易瞬间导致网络阻塞,特别是当用户并发访问很大的时时候,情况更加明显。
(3),数据显示问题。如果一次性把成千上万条记录用一个页面或窗口呈献给查询用户,容易造成客户视觉疲劳,所以我们要考虑分页显示。
针对不同的问题,我们解决问题的方法也不尽相同。对待大结果集数据分页的处理上,我们一般的设计策略有以下3种方式:
(一),把结果集所有的数据一次性查询出来,放在应用服务器内存中,然后再分页处理。
这是一种很常见的思路,它的优点是:解藕了ResultSet操作类和数据库查询游标的关系,使得数据库连接得到有效的释放,提高了数据库并发访问的性能。同时,把数据导在本地,处理十分简单。由于数据都在内存中,翻页查询没有必要再从数据库请求数据,直接从内存获取就可以了,因而,响应更快。其缺点有:由于数据缓存在内存中,客户查询有可能看到的是过期数据;如果查询结果集数据量非常大的话,第一次查询遍历结果集会很耗时间,而且缓存这样大的数据也会造成内存消耗过大,使得JVM的效率明显降低。所以,这种方式对于查询结果在1000条以内的“小”结果集,而且数据完整性要求不高的情况下,可以采用,而且也十分简单有效。
(二),对查询结果集进行业务分析,针对查询客户的需要,把客户最为关心的数据首先呈献给客户,对于一些老数据,放入历史库,另外提供一个历史查询模块给客户使用。
这其实是把查询结果集按照客户的需要拆分开,避免了问题的主要矛盾,当然,前提是假设了用户实时关心的数据并不多。例如,对新闻数据查询,客户可能最关心的就是近3天的新闻信息,其它过期新闻可能1个月也不会查询一次。这个方式主要基于业务上设计策略,实现的技术也是甚为灵活。
(三),查询结果集所有的数据仍然留在数据库服务器端。每次翻页,我们都根据页面大小只从数据库服务器里面检索并返回块区数据,直到检索完所有的记录。
由于每次查询的记录数很少,网络传输数据量不大,不会造成阻塞。而且,数据都是由数据库统一提供,不会存在数据过期问题。应该说,这种方式是解决数据分页的理想方式。
实现方式(三)的技术常见有:
■直接使用JDBC底层的ResultSet接口,利用游标来处理。
ResultSet本质是在数据库服务器上建立游标,然后通过行位置定位数据记录。这种方法,当客户第一请求数据时,就执行查询语句,获得ResultSet对象,保存在会话中,以后分页获取数据时都是通过ResultSet对象来定位和获取指定位置的记录。最后,当客户不再进行分页查询的时候,就关闭会话,释放数据库连接和ResultSet等等数据访问资源。
优点:减少sql语句查询的次数,利用标准API实现,便于系统移植。
缺点:每一个用户查询都要占用一个数据库连接和结果集游标。当并发用户量大的时候,会浪费数据库连接资源,使数据库响应变慢,性能降低;另外,从系统设计的角度考虑,开发人员需要操作ResultSet和维护其状态,使得无法从结果集的处理独立出来,对系统框架的抽象设计很是不利;而且,ResultSet在数据定位上的操作也很是笨拙。
所以这方法基本上不值得提倡。
■ 利用各种数据库服务器自己提供的对查询结果集的可定位行范围的接口技术来实现。
客户发出请求,数据库就根据查询请求的行范围参数,检索出所需的记录返回给客户端。客户获取记录集后就释放相关的数据访问资源,从而避免了对并发访问的影响。
优点:技术简单,直接利用了数据库产品特性实现,java开发人员无需关心其具体的实现原理;充分利用了数据库的连接资源;系统查询响应速度快;解藕了ResultSet与数据结果集的关系,便于系统框架的抽象设计。
缺点:需要涉及专有的数据库技术,对java开发人员要求高。每翻页一次,就要查询数据库一次;通用性不强,因为每种数据库的行范围定位技术都不一样,需要重新抽象。
虽然对开发人员数据库知识要求高,但这种方法往往是最为有效的解决方法。我们只要根据经验,把各种数据库的行范围技术总结一次,我们就重复利用,数据分页的问题因而也就变得十分简单了。BJAF数据存取框架总结来各个数据库系统分页特有技术,封装成 Pagination 查询分页组件,有效地解决来持久层数据库查询分页编程难题,组件UML结构图如下:
IPagination接口定义了 page(pInfo : PageParameter) : PageResult 分页方法,PaginationFactory用来解耦客户端与各个实现类之间的调用关系;PageParameter是分页参数对象,负责分页请求参数的构建和组织;PageResult是分页查询结果信息对象;PageHelper是计算分页的助手类。
假设前面查询EMP表薪水>2000的雇员记录数超过3000条,现在采取每页20返回条记录进行分页查询,其实现代码如下:
public static void main_testPagination(String[] args) { // 建立一个分页参数对象 PageParameter param = new PageParameter(); param.setDataSourceName(Const.dataSourceName); // 设置数据源 param.setUserSql("select empno,ename,job,mgr,hiredate,sal,comm,deptno from emp where sal>=?"); // 查询语句 param.addParameter(new SqlParameter(SqlType.FLOAT, new Float(2000)));// 设置sql参数 param.setPageNumber(1);// 设置查询页号码(现返回第1页数据) param.setPageSize(20);// 设置每页返回数量大小(条数) // 利用PaginationFactory获取Oracle分页实现的实例 IPagination pageOpt = PaginationFactory .getPaginationInstance(PaginationFactory.ORACLE_ID); // 执行分页查询,并返回查询结果信息对象 PageResult result = pageOpt.page(param); // 打印此结果信息对象 System.out.println(result.getCurPageNumber());// 当前页号 System.out.println(result.getCurPageSize());// 当前页数量大小 System.out.println(result.getCurPos());// 当前记录集位置 System.out.println(result.getFirstPageNumber());// 首页号码 System.out.println(result.getLastPageNumber());// 最后页号码 System.out.println(result.getNextPageNumber());// 下一页号码 System.out.println(result.getPrePageNumber());// 前一页号码 System.out.println(result.getPageAmount());// 总页数 System.out.println(result.getRecordAmount());// 总记录数 // 利用RsDataSet解析查询记录集 RsDataSet rs = new RsDataSet(result.getSqlResultSet());// 记录数据 for (int i = 0; i < rs.rowCount; i++) { Emp emp = new Emp(); rs.autoFillRow(emp); System.out.println(emp.getEmpNo()); System.out.println(emp.getEname()); System.out.println(emp.getHireDate()); System.out.println(emp.getJob()); System.out.println(emp.getMgr()); System.out.println(emp.getSal()); System.out.println(emp.getComm()); System.out.println(emp.getDeptNo()); System.out.println("--"); emp = null; rs.next(); } rs.clearAll(); }
注:SqlServerPaginationImp和SybasePaginationImp实现依赖与 Sp_Pagination 数据库存储过程,具体请参考java doc的api文档。