UML软件工程组织

提升数据访问层的性能(二)

3. 选择优化性能的功能


3.1. 使用参数标记作为存储过程的参数

调用存储过程时,用参数标记做为参数尽量不要用字符做参数。JDBC驱动调用存储过程时要么象执行其他SQL查询一样执行该过程,要么通过RPC直接调用来优化执行过程。如果象SQL查询那样执行存储过程,数据库服务器先解析该语句,验证参数类型,然后把参数转换成正确的数据类型,显然这种调用方式不是最高效的。
SQL语句总是做为一个字符串送到数据库服务器上,例如,
“{call getCustName (12345)}”。
在这种情况下,即使程序员设想给getCustName唯一的参数是整型,事实上参数传进数据库的仍旧是字符串。数据库服务器解析该语句,分离出单个参数值12345,然后在把过程当作SQL语言执行之前,将字符串“12345”转换成整型值。
通过RPC在数据库服务器中调用存储过程,就能避免使用SQL字符串带来的开销。
情形1
在这个例子中,就不能使用服务器端的RPC优化调用存储过程。调用的过程包括解析语句,验证参数类型,在执行过程之前把这些参数转换成正确的类型。
CallableStatement cstmt = conn.prepareCall ( "{call getCustName (12345)}"); ResultSet rs = cstmt.executeQuery ();
情形2
在这个例子中,可以使用服务器端RPC优化调用存储过程。由于应用避免了文字参数传递带来的开销,且JDBC能以RPC方式直接在数据库中调用存储过程来优化执行,所以,执行时间也大大地缩短了。
CallableStatement cstmt =
conn.prepareCall ( "{call getCustName (?)}");cstmt.setLong (1,12345);
ResultSet rs = cstmt.executeQuery();
JDBC根据不同的用途来优化性能,所以我们需要根据用途在PreparedStatement对象和Statement对象之间做出选择。如果执行一个单独的SQL语句,就选择Statement对象;如果是执行两次或两次以上就选择PreparedStatement对象。
有时,为了提高性能我们可以使用语句池。当使用语句池时,如果查询被执行一次且可能再也不会被执行,那就使用Statement对象。如果查询很少被执行,但在语句池的生命期内可能再一次被执行,那么使用PreparedSatement。在相同的情形下,如果没有语句池,就使用Statement对象。

3.2. 用批处理而不是用PreparedStatement语句

更新大量的数据通常是准备一个INSERT语句并多次执行该语句,结果产生大量的网络往返。为了减少JDBC调用次数和提高性能,你可以使用PreparedStatement对象的addBatch()方法一次将多个查询送到数据库里。例如,让我们比较一下下边的例子,情形1和情形2。
情形1:多次执行PreparedStatement语句
PreparedStatement ps = conn.prepareStatement("INSERT into employees values (?, ?, ?)");
for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.executeUpdate();
}
情形2:使用批处理
PreparedStatement ps =
conn.prepareStatement( "INSERT into employees values (?, ?, ?)");
for (n = 0; n < 100; n++) {
ps.setString(name[n]);
ps.setLong(id[n]);
ps.setInt(salary[n]);
ps.addBatch();
}
ps.executeBatch();
在情形1中,一个PreparedStatement用于多次执行一个INSERT语句。在这个情况下,为了100次插入需要101次网络往返,其中一次用于准备语句,额外100次网络往返用于执行每一个操作。当addBatch()方法的时候,如情形2所述,仅需要两个网络往返,一个准备语句,另一个执行批处理。尽管使用批处理需要更多的数据库CPU运算开销,但性能可由减少的网络往返获得。记住要让JDBC驱动在性能方面有良好的表现,就要减少JDBC驱动和数据库服务器之间的网络通讯量。

3.3. 选择合适的游标

选择合适的游标能提高应用的灵活性。本节总结了三类游标的性能问题。向前游标对连续读表中所有的行提供了优秀的性能。就检索表数据而言,没有一个检索数据的方法要比向前游标更快。然而,当应用必须处理非连续方式的行时,就不能使用它。
对需要数据库高层次的并发控制和需要结果集向前和向后滚动能力的应用而言,JDBC驱动使用的无感知游标是最为理想选择。对无感知游标的第一次请求是获取所有的行(或者当JDBC使用“懒惰”方式读时,可以读取部分行)并将它们存储在客户端。那么,第一次请求将会非常慢,特别是当长数据被检索到的时候。后续的请求不再需要网络交通(或当驱动采用懒惰方式时,只有有限的网络交通)并处理得很快。由于第一次请求处理缓慢,无感知游标不应该用于一行数据的单个请求。当要返回长数据时,内存很容易被耗尽,所以开发人员也应该避免使用无感知游标。一些无感知游标的实现是把数据缓存在数据库中的零时表中,避免了性能问题,但是,大多数是把信息缓存在应用本地。
无感知游标,有时又叫键集驱动的游标,使用标识符,如已经存在于你数据库中的ROWID。当你通过结果集滚动的时候,适合于标识符的数据会被检索到。由于每个请求都产生网络交通量,所以性能将会非常差。然而,返回非连续行不会更多的影响性能。
为了更进一步说明,我们来看一个通常返回应用1000行数据的应用。在执行时或第一行被请求时,JDBC不会执行由应用提供的SELECT语句。而是JDBC驱动用键标识符替换查询的SELECT列表,例如,ROWID。这个修改的查询将会被驱动执行,并且所有1000键值将会被从数据库中检索出来并被驱动缓存。每一个来自应用对结果行的请求将转到JDBC驱动,为了返回合适的行,JDBC在它本地缓存中查询键值,构造一个类似于“WHERE ROWID=?”包含WHERE的优化的语句,执行这个修改了查询,然后从服务器上检索单个结果行。
当应用使用来自缓存中的无感知(Insensitive)游标数据时,有感知(Sensitive)游标在动态情形下就是首选的游标模式。

3.4. 有效地使用get方法

JDBC提供了很多从结果集中检索数据的方法,例如getInt(),getString(),以及getObject()。getObject()方法是最普通的方法,但在没有说明非默认映射时提供了最差的性能。这是因为为了确定被检索值的类型和产生合适的映射,JDBC驱动必须做额外的处理。所以,总是使用能明确数据类型的方法。
为了更好地提高性能,请提供被检索列的列数字,例如,getString(1),getLong(2),和getInt(3),而不是列名。如果列数字没有说明,网络流量是不受影响的,但转换和查找的成本上升了。例如,假设你使用getString(“foo”)…驱动可能不得不将列的标识符foo转换成大写(如果必要),并在列列表中用“foo”和所有的列名比较。如果提供了列数字,很大部分的处理都被节省了。
例如,假如你有一个15列100行的结果集,列名没有包括在结果集中。你感兴趣的有三列,EMPLOEEMENT(字符串),EMPLOYEENUMBER(长整型),和SALARY(整型)。如果你说明了getString(“EmployeeName”),getLong(“EmployeeNumber”)和getInt(“Salary”),每列的列名必须转换成和数据库元数据中匹配的大小写,毫无疑问查询将相应的增加。如果你说明getString(1),getLong(2),和getInt(15),性能将会大大地提高。

3.5. 检索自动产生的键

许多数据库已经隐藏了描述表中每行唯一键的列(又叫伪列)。通常,由于伪列描述了数据的物理磁盘地址,故而在查询中使用这种类型的列存取行是最快的方式。在JDBC3.0以前,应用仅能在插入数据之后立即执行SELECT语句检索到伪列的值。
For example:
//insert rowint
rowcount = stmt.executeUpdate ( "insert into LocalGeniusList (name) values ('Karen')");
// now get the disk address - rowid - for the newly inserted row
ResultSet rs = stmt.executeQuery ( "select rowid from LocalGeniusList where name = 'Karen'");
这个检索伪列的方法有两个主要的缺点。第一,检索伪列需要通过网络把一个单独的查询语句发送到服务器上执行。第二,由于表中可能没有主键,查询条件可能不能唯一地确定行。在后边的情形中,多个伪列值被返回,应用或许不能确定哪个是最近插入的行。
JDBC规范一个可选的特性是当行插入表时,能检索到行的自动产生的键信息。
For example:
int rowcount = stmt.executeUpdate ( "insert into LocalGeniusList (name) values ('Karen')",
// insert row AND return
keyStatement.RETURN_GENERATED_KEYS);
ResultSet rs = stmt.getGeneratedKeys ();
// key is automatically available
即便该表没主键,这都给应用提供了一个唯一确定行值的最快方法。当存取数据时,检索伪列键的能力给JDBC开发人员提供了灵活性并创造了性能。

4. 管理连接和数据更新


4.1. 管理连接

连接管理的好坏直接影响到应用的性能。采用一次连接创建多个Statement对象的方式来优化你的应用,而不是执行多次连接。在建立最初的连接之后要避免连接数据源。
一个不好的编码习惯是执行SQL语时连接和断开好几次。一个连接对象可以有多个Statement对象和它关联。由于Statement对象是定义SQL语句信息的内存存储,它能管理多个SQL语句。此外,你可以使用连接池来显著地提高性能,特别是对那些通过网络连接或通过WWW连接的应用。连接池让你重用连接,关闭连接不是关闭与数据库的物理连接,而是将用完的连接放到连接池中。当一个应用请求一个连接时,一个活动的连接将从连接池中取出重用,这样就避免了创建新连接的而产生的网络I/O。

4.2. 在事务中管理提交

由于磁盘I/O和潜在的网络I/O,提交事务往往要慢。经常使用WSConnection.setAutoCommit(false)来关闭自动提交设置。
提交实际上包括了什么呢?数据库服务器必须刷新包含更新的和新数据的磁盘上的每一个数据页。这通常是一个对日志文件连续写的过程,但也是磁盘I/O。默认情况下,当连接到数据源时,自动提交是打开的,由于提交每个操作需要大量的磁盘I/O,自动提交模式通常削弱了性能。此外,大部分数据库没有提供本地的自动提交模式。对这种类型的服务器,JDBC驱动对每一个操作必须明确地给服务器送出COMMIT语句和一个BEGIN TRANSACTION。
尽管使用事务对应用的性能有帮助,但不要过度地使用。由于为了防止其他用户存取该行而在行上长时间的持有锁将减少吞吐量。短时间内提交事务可以最大化并发量。

4.3. 选择正确的事务模式

许多系统支持分布式事务;也就是说,事务能跨越多个连接。由于记录日志和所有包含在分布式事务中组件(JDBC驱动,事务监视器和数据库系统)之间的网络I/O,分布式事务要比普通的事务慢四倍。除非需要分布式事务,否则尽量避免使用它们。如果可能就使用本地事务。应该注意的是许多Java应用服务器提供了一个默认的利用分布式事务的事务行为。为了最好的系统性能,把应用设计在运行在单个连接对象之下,除非必要避免分布式事务。

4.4. 使用updateXXX方法

尽管编程的更新不适用于所有类型的应用,但开发人员应该试着使用编程的更新和删除,也就是说,使用ResultSet对象的updateXXX()方法更新数据。这个方法能让开发人员不需要构建复杂的SQL语句就能更新数据。为了更新数据库,在结果集中的行上移动游标之前,必须调用updateRow()方法。
在下边的代码片断中,结果集对象rs的Age列的值使用getInt()方法检索出来,updateInt()方法用于用整型值25更新那一列。UpdateRow()方法用于在数据库中更新修改了值的行。
int n = rs.getInt("Age");
// n contains value of Age column in the resultset rs...
rs.updateInt("Age", 25);
rs.updateRow();
除了使应用更容易地维护,编程更新通常产生较好的性能。由于指针已经定位在被更新的行上,定位行的所带来的开销就避免了。

4.5. 使用getBestRowIdentifier()

使用getBestRowIdentifier()(请参阅DatabaseMetaData接口说明)确定用在更新语句的Where子句中的最优的列集合。伪列常常提供了对数据的最快的存取,而这些列仅能通过使用getBestRowIdentifier()方法来确定。
一些应用不能被设计成利用位置的更新和删除。一些应用或许通过使用可查询的结果列,如调用getPrimaryKeys()或者调用getIndexInfo()找出可能是唯一索引部分的列,使Where子句简洁化。这些方法通常可以工作,但可能产生相当复杂的查询。看看下边的例子:
ResultSet WSrs = WSs.executeQuery ("SELECT first_name, last_name, ssn, address, city, state, zip FROM emp");
// fetch data...
WSs.executeUpdate ("UPDATE EMP SET ADDRESS = ? WHERE first_name = ? and last_name = ? and ssn = ? and address = ? and city = ? and state = ? and zip = ?");
// fairly complex query
应用应该调用getBestRowIdentifier()检索最优集合的能确定明确的记录的列(可能是伪列)。许多数据库支持特殊的列,它们没有在表中被用户明确地定义,但在每一个表中是“隐藏”的列(例如,ROWID和TID)。由于它们是指向确切记录位置的指针,这些伪列通常给数据提供了最快的存取。由于伪列不是表定义的部分,它们不会从getColumns中返回。为了确定伪列是否存在,调用getBestRowIndentifier()方法。
再看一下前边的例子:
...
ResultSet WSrowid = getBestRowIdentifier(... "emp", ...);
...
WSs.executeUpdate ("UPDATE EMP SET ADDRESS = ? WHERE ROWID = ?";
// fastest access to the data!
如果你的数据源没有包含特殊的伪列,那么getBestRowIdentifier()的结果集由指定表上的唯一索引组成(如果唯一索引存在)。因此,你不需要调用getIndexInfo来找出最小的唯一索引。

 

版权所有:UML软件工程组织