服务热线:13616026886

技术文档 欢迎使用技术文档,我们为你提供从新手到专业开发者的所有资源,你也可以通过它日益精进

位置:首页 > 技术文档 > JAVA > 新手入门 > 基础入门 > 查看文档

如何测定jdbc的性能


  java数据库连接(jdbc)被广泛用在java应用程序中。在本篇文章中,我们将讨论如何测定jdbc的性能,如何判断jdbc子系统中的哪一部分需要进行优化。

  核心的java.sql界面

  我们的目的是提高应用程序的性能。一般情况下,我们需要对应用程序进行分析,找出其中的瓶颈。当然了,要对分布式应用程序进行有效的分析是比较困难的,i/o是分析的一个重点,这是由分布式应用程序的特点决定的,分布式应用程序中的线程需要花费大量的时间等待i/o操作。目前还不清楚线程因等待读、写操作而阻塞是瓶颈的一部分呢还是一个无关紧要的小问题。在进行分析时,有一个独立的通信系统测试标准是重要的。那么在测试jdbc子系统的性能时,我们应当测试哪些指标呢?

  在java.sql软件包中,有三个接口组成了jdbc的核心:connection、statement和resultset。与数据库的正常交互包括下面的几部分:

   ?从数据库驱动程序中获得一个connection对象。

   ?从connection对象中获取能够执行指定的sql语句的statement对象

   ?如果sql语句需要从数据库中读取数据,则使用statement对象获取一个提供对数据库中的数据进行访问的resultset对象。

  下面的例子通过访问指定数据库表的每行记录的所有域、将每行的数据存储到string []、并将所有的行放到一个向量中,演示了标准的数据库交互过程。

public static vector getatable(string tablename, connection connection)
throws sqlexception
{
string sqlquery = "select * from " + tablename;
statement statement = connection.createstatement();
resultset resultset = statement.executequery(sqlquery);
int numcolumns = resultset.getmetadata().getcolumncount();
string[] arow;
vector allrows = new vector();
while(resultset.next())
{
arow = new string[numcolumns];
for (int i = 0; i < numcolumns; i++)
file://resultset的访问是从1开始的,数组是从0开始的。
arow[i] = resultset.getstring(i+1);
allrows.addelement(arow);
}
return allrows;
}

  在java.sql或其他的sdk中没有connection、statement和resultset这三个对象的具体实现,这些对象以及其他的jdbc接口都是由数据库驱动程序的厂商开发的,并被作为数据库驱动程序的一部分包括在驱动程序软件包中。如果要打印出connection对象或使用的其他对象的类名,可能会看到类似xxxconnection、xxxstatement、xxxconnectionimpl、xxxstatementimpl等字符串,其中的xxx就是正在使用的数据库的名字,例如oracle。

  如果我们要测试例子中getatable()方法的jdbc的性能,可以简单地在该方法的开始处和末尾处添加system.currenttimemillis(),二者之间的时间差就是getatable()方法执行所使用的时间。只要数据库的交互过程与其他过程没有搅和在一起,就可以使用这种方法测试一个方法的jdbc性能。但通常情况下,java应用程序的的数据库交互过程分布在许多类的许多方法中,而且很难将数据库交互过程单独分离出来。那么在这种情况下我们应该如何测试数据库交互过程的性能呢?

  一个理想的方法是在所有的jdbc类中都内置测量性能的能力,然后可以在需要对其性能进行监测时简单地打开监测功能就可以了。正常情况下,jdbc类没有提供这种能力,但我们可以使用具备这种功能的类来替换它们,我们替换类的目标是提供与proxy非常相似的对象。

  使用一个接口的专用封装对象封装该接口的对象是一种有多种用途的成熟技术,collection类同步的封装对象就是最著名的一个例子,但还有其他许多用途。sdk中甚至有一个专门在运行时才生成封装对象的类:java.lang.reflect.proxy类。封装对象也被称作代理对象,如果在本篇文章中使用代理对象这个术语,会使对封装jdbc对象的解释更复杂,因此,在本篇文章中仍然会坚持使用封装类。

  要在上述功能的基础上添加测试数据库交互过程的功能,还需要对应用程序的其他部分作一些改变,很明显的是,这样作需要一定的代价。

  幸运的是,当一个框架象jdbc那样几乎完全采用接口来定义时,要用另外的实现替换其中的作一个类就相当简单了。我们可以使用一个封装类替换一个接口的任何一种实现,该封装类封装原有的类,并转发所有对原来类的方法的调用。在本篇文章中,我们可以使用一个封装类替换掉jdbc类,将我们监测jdbc性能的功能放置在封装类中,然后使监测功能随整个应用程序的执行而执行。


  封装connection类

  我们将首先讨论connection类的封装。下面的connectionwrapper类实现了connection类,该类有一个connection类的实例变量和使用构建器的参数初始化实例变量的构建器,大多数的connection类的方法被简单地定义为将调用托付给实例变量:

package tuning.jdbc;

import java.sql.*;
import java.util.map;

public class connectionwrapper implements connection
{
protected connection realconnection;

public connection realconnection () {
return realconnection;
}

public connectionwrapper (connection connection) {
realconnection = connection;
}

public void clearwarnings() throws sqlexception {
realconnection.clearwarnings();
}

public void close() throws sqlexception {
realconnection.close();
}

public boolean isclosed() throws sqlexception {
return realconnection.isclosed();
}

public void commit() throws sqlexception {
realconnection.commit();
}

...

  我省略了大部分的方法,但它们都符合下面的的模板,在需要使用从数据库驱动程序中获取的connection对象的地方,我们可以简单地使用connectionwrapper封装connection对象,而使用connectionwrapper对象。无论在哪里获取了connection对象,我们都需要在该处添加下面的二行代码:

connection dbconnection = getconnectionfromdriver();
dbconnection = new connectionwrapper(dbconnection);

  获得连接是该应用程序中唯一需要改变的部分,这要求发现所有获得一个connection对象的调用,并对该调用进行编辑。然而,大多数的应用程序使用一个集中的代理类提供connection对象,在这种情况下,在应用程序中使用connectionwrapper就非常简单了。该代理类需要频繁地访问一个connection对象池,因此在将一个connection对象释放回connection对象池中时,还需要作一些额外的工作,因为connection对象首先需要被解包,例如:

public static void releaseconnection(connection conn)
{
if (conn instanceof connectionwrapper)
conn = ( (connectionwrapper) conn).realconnection();
...
}

  我们还没有真正地完成connectionwrapper类,connectionwrapper类中有一些方法不能简单地托付,这些就是提供各种statement对象的方法:

public statement createstatement() throws sqlexception {
return new statementwrapper(realconnection.createstatement(), this);
}

public statement createstatement(int resultsettype,
int resultsetconcurrency) throws sqlexception {
return new statementwrapper(
realconnection.createstatement(resultsettype,
resultsetconcurrency), this);
}

public callablestatement preparecall(string sql) throws sqlexception {
return new callablestatementwrapper(
realconnection.preparecall(sql), this, sql);
}

public callablestatement preparecall(string sql, int resultsettype,
int resultsetconcurrency) throws sqlexception {
return new callablestatementwrapper(
realconnection.preparecall(sql, resultsettype,
resultsetconcurrency), this, sql);
}

public preparedstatement preparestatement(string sql)
throws sqlexception {
return new preparedstatementwrapper(
realconnection.preparestatement(sql), this, sql);
}

public preparedstatement preparestatement(string sql, int resultsettype,
int resultsetconcurrency) throws sqlexception {
return new preparedstatementwrapper(
realconnection.preparestatement(sql, resultsettype,
resultsetconcurrency), this, sql);
}

  如上所示,我们需要定义三种statement封装类,另外,我们还需要为databasemetadata定义一个封装类,该封装类必须是完备的,因为databasemetadata能够返回用来创建databasemetadata的connection对象,因此我们需要确保connection对象是经过封装过的,而不是我们没有封装过的connection对象。

public databasemetadata getmetadata() throws sqlexception {
return new databasemetadatawrapper(
realconnection.getmetadata(), this);
}
封装statement类
statement、preparedstatement和callablestatement这三个statement类的封装类相似:
public class statementwrapper implements statement
{
protected statement realstatement;
protected connectionwrapper connectionparent;

public statementwrapper(statement statement, connectionwrapper parent)
{
realstatement = statement;
connectionparent = parent;
}

public void cancel() throws sqlexception {
realstatement.cancel();
}

...

  我选择了将preparedstatementwrapper实现为statementwrapper的一个子类,但这并不是必须的。我们可以将preparedstatement作为object的一个子类,实现所有的要求的方法,而不是继承statement类的方法。

public class preparedstatementwrapper extends statementwrapper implements preparedstatement
{
preparedstatement realpreparedstatement;
string sql;
public preparedstatementwrapper(preparedstatement statement, connectionwrapper parent, string sql)
{
super(statement, parent);
realpreparedstatement = statement;
this.sql = sql;
}

public void addbatch() throws sqlexception {
realpreparedstatement.addbatch();
}

  同样地,我选择了将callablestatementwrapper实现为preparedstatementwrapper的一个子类:

public class callablestatementwrapper extends preparedstatementwrapper implements callablestatement
{
callablestatement realcallablestatement;
public callablestatementwrapper(callablestatement statement, connectionwrapper parent, string sql)
{
super(statement, parent, sql);
realcallablestatement = statement;
}

public array getarray(int i) throws sqlexception {
return new sqlarraywrapper(realcallablestatement.getarray(i), this, sql);
}

  这一次,我们并没有写出全部的代码。这些statement封装类中的一些方法不能被简单地托付。第一,是一个返回connection对象的方法,我们希望返回connectionwrapper对象,当然了,这非常容易作到。下面是statementwrapper中的方法:

public connection getconnection() throws sqlexception {
return connectionparent;
}

  第二,我们有返回resultsets的方法。这些方法需要返回resultset的封装类。为了保证resultsetwrapper的一致性,我们在statementwrapper中添加了一个传递给其构建器的lastsqlstring实例变量。当我们对特定的sql语句进行性能监测时,该实例变量就非常有用了。返回resultssets的方法如下所示:

//statementwrapper方法
public resultset getresultset() throws sqlexception {
return new resultsetwrapper(realstatement.getresultset(), this, lastsql);
}

public resultset executequery(string sql) throws sqlexception {
return new resultsetwrapper(realstatement.executequery(sql), this, sql);
}

//preparedstatementwrapper方法
public resultset executequery() throws sqlexception {
return new resultsetwrapper(realpreparedstatement.executequery(), this, sql);
}

  第三,一些方法使用了java.sql.array对象。由于这些array对象能够返回resultset,因此我们再次需要提供一个array封装对象,以便返回resultsetwrapper而不是普通的resultsets。另外,我们还需要处理array对象被传递给setarray()方法的情况:如果传递的是array封装对象,则在被传递给preparedstatement前,该对象需要被解封装:

public void setarray(int i, array x) throws sqlexception {
if (x instanceof sqlarraywrapper)
realpreparedstatement.setarray(i, ((sqlarraywrapper) x).realarray);
else
realpreparedstatement.setarray(i, x);
}

public array getarray(int i) throws sqlexception {
return new sqlarraywrapper(realcallablestatement.getarray(i), this, sql);
}

  最后,我们创建这些封装类的目的是能够实现性能的监测。我们要在下面的方法中添加测试jdbclogger类性能的功能,这样每个方法都有一个对被封装在测试调用中的真正执行方法的调用。我们将sql字符串和当前的线程传递给测试调用,因为对于任何类型的测试调用来说,这二个参数都是十分重要的,尤其是在测量过程运行的时间时更是如此。另外,需要注意的是,我还重新定义了返回resultsets的executequery()方法,以便在其中插入测试类:

//statementwrapper方法
public void addbatch(string sql) throws sqlexception {
realstatement.addbatch(sql);
lastsql = sql;
}

public boolean execute(string sql) throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, sql);
boolean b = realstatement.execute(sql);
jdbclogger.endlogsqlquery(t, sql);
lastsql = sql;
return b;
}

public int[] executebatch() throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, "batch");
int[] i = realstatement.executebatch();
jdbclogger.endlogsqlquery(t, "batch");
return i;
}

public resultset executequery(string sql) throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, sql);
resultset r = realstatement.executequery(sql);
jdbclogger.endlogsqlquery(t, sql);
lastsql = sql;
return new resultsetwrapper(r, this, sql);
}

public int executeupdate(string sql) throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, sql);
int i = realstatement.executeupdate(sql);
jdbclogger.endlogsqlquery(t, sql);
lastsql = sql;
return i;
}


file://preparedstatementwrapper方法
public boolean execute() throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, sql);
boolean b = realpreparedstatement.execute();
jdbclogger.endlogsqlquery(t, sql);
return b;
}

public resultset executequery() throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, sql);
resultset r = realpreparedstatement.executequery();
jdbclogger.endlogsqlquery(t, sql);
return new resultsetwrapper(r, this, sql);
}

public int executeupdate() throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlquery(t, sql);
int i = realpreparedstatement.executeupdate();
jdbclogger.endlogsqlquery(t, sql);
return i;
}

  封装resultset类

  resultsetwrapper类也主要包括托付方法:


public class resultsetwrapper implements resultset
{
resultset realresultset;
statementwrapper parentstatement;
string sql;

public resultsetwrapper(resultset resultset, statementwrapper statement, string sql) {
realresultset = resultset;
parentstatement = statement;
this.sql = sql;
}

public boolean absolute(int row) throws sqlexception {
return realresultset.absolute(row);
}

...

  其中也有一些方法不是简单的托付方法,getstatement()方法返回生成resultset的statement对象,我们需要让它返回statementwrapper对象:

public statement getstatement() throws sqlexception {
return parentstatement;
}
the getarray() methods need to return a wrapped array object:

public array getarray(int i) throws sqlexception {
return new sqlarraywrapper(realresultset.getarray(i), parentstatement, sql);
}

public array getarray(string colname) throws sqlexception {
return new sqlarraywrapper(realresultset.getarray(colname), parentstatement, sql);
}

  最后,我们需要添加测试过程。许多开发人员都错误地认为,不同的statement.execute*()方法都会引起数据库交互过程带来的负担,对于数据库的更新和读取少量的数据库记录而言,这是正确的。如果读取的数据库记录的量较大,resultset.next()需要大量的时间从数据库中读取记录。如果读取的记录太多,resultset.next()调用所需要的时间就会多于sql语句执行的时间。因此,测试resultset.next()调用的时间也就是理所当然的了。

public boolean next() throws sqlexception {
thread t = thread.currentthread();
jdbclogger.startlogsqlnext(t, sql);
boolean b = realresultset.next();
jdbclogger.endlogsqlnext(t, sql);
return b;
}

  如果需要,还有一些resultset调用可以测量,例如previous()、insertrow()等,但大多数的应用程序只需要对next()进行测量。


  jdbc封装类架构

  上面讨论了需要封装的类,我没有明确地说明array和databasemetadata的封装类,但它们都比较简单,只需要返回resultsetwrappers和connectionwrappers而不是resultsets和connections类。使用封装对象测试数据库交互过程性能的技术适用于jdbc 1、jdbc 2和未来的jdbc 3,它们在接口定义方面互不相同(因此需要不同的封装类。但我们可以用同一种方式创建所有不同版本下的封装类。

  我没有讨论的是jdbclogger,该类的一个简单的实现中不调用测试方法,但将不提供测试功能:

package tuning.jdbc;

public class jdbclogger
{
public static void startlogsqlquery(thread t, string sql) {}
public static void endlogsqlquery(thread t, string sql) {}
public static void startlogsqlnext(thread t, string sql) {}
public static void endlogsqlnext(thread t, string sql) {}

}

  一个更有用的定义是测试查询的时间。下面的方法记录查询开始时的时间,并在查询结束时得出使用的时间。由于假定在同一个线程中sql查询不能递归(一般情况下都是这样的),下面的方法是相当简单的:

private static hashtable querytime = new hashtable();

public static void startlogsqlquery(thread t, string sql)
{
if (querytime.get(t) != null)
system.out.println("warning: overwriting sql query log time for " + sql);
querytime.put(t, new long(system.currenttimemillis()));
}

public static void endlogsqlquery(thread t, string sql)
{
long time = system.currenttimemillis();
time -= ((long) querytime.get(t)).longvalue();
system.out.println("time: " + time + " millis for sql query " + sql);
querytime.remove(t);
}

  使用jdbclogger类中的这些方法的输出将如下所示:

  time: 53 millis for sql query select * from jacktabl

  对于每次查询执行来说,这将使我们能够精确地测试sql查询所使用的时间,也能够计算出jdbclogger类中所有查询所需要的时间。我经常测试的是最小、最大、平均、平均偏差等值,这些值在测试大规模的系统的性能时更有用。


  使用jdbc封装类框架

  我们已经介绍了非常有用的在应用程序的开发和布置阶段测试jdbc调用性能的方法。由于封装类比较简单,而且功能强大,又不需要对应用程序进行大量的修改,它们可以被保留在已经布置好的应用程序中,创建一个可配置的jdbclogger类将使我们能够根据自己的需要开启或关闭测试功能。

  在开发阶段,由于能够计算出累积的时间代价,我们能够利用这些类辨别出个别的需要较大时间代价的数据库交互过程和重复的数据库交互过程,哪个的时间代价更大。辨别出时间代价较大的数据库交互过程是我们改进应用程序性能的第一步。在开发阶段,这些封装类可以用来发现应用程序的理论性能和实际性能之间的差距,有助于我们分析为什么会有差距。

  在利用这些类找出jdbc的性能瓶颈在哪里后,我们就可以对数据库的接口进行调整了。我将在以后的文章中继续讨论jdbc性能的技术。


扫描关注微信公众号