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.*; public class ConnectionWrapper implements Connection public Connection realConnection () { public ConnectionWrapper (Connection Connection) { public void clearWarnings() throws SQLException { public void close() throws SQLException { public boolean isClosed() throws SQLException { public void commit() throws SQLException { |
我省略了大部分的方法,但它們都符合下面的的模板,在需要使用從數據庫驅動程序中獲取的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 { public Statement createStatement(int resultSetType, public CallableStatement prepareCall(String sql) throws SQLException { public CallableStatement prepareCall(String sql, int resultSetType, public PreparedStatement prepareStatement(String sql) public PreparedStatement prepareStatement(String sql, int resultSetType, |
如上所示,我們需要定義三種Statement封裝類,另外,我們還需要為DatabaseMetaData定義一個封裝類,該封裝類必須是完備的,因為DatabaseMetaData能夠返回用來創建DatabaseMetaData的Connection對象,因此我們需要確保Connection對象是經過封裝過的,而不是我們沒有封裝過的Connection對象。
public DatabaseMetaData getMetaData() throws SQLException { public StatementWrapper(Statement statement, ConnectionWrapper parent) public void cancel() throws SQLException { ... |
我選擇了將PreparedStatementWrapper實現為StatementWrapper的一個子類,但這并不是必須的。我們可以將PreparedStatement作為Object的一個子類,實現所有的要求的方法,而不是繼承Statement類的方法。
public class PreparedStatementWrapper extends StatementWrapper implements PreparedStatement public void addBatch() throws SQLException { |
同樣地,我選擇了將CallableStatementWrapper實現為PreparedStatementWrapper的一個子類:
public class CallableStatementWrapper extends PreparedStatementWrapper implements CallableStatement public Array getArray(int i) throws SQLException { |
這一次,我們并沒有寫出全部的代碼。這些Statement封裝類中的一些方法不能被簡單地托付。第一,是一個返回Connection對象的方法,我們希望返回ConnectionWrapper對象,當然了,這非常容易作到。下面是StatementWrapper中的方法:
public Connection getConnection() throws SQLException { return connectionParent; } |
第二,我們有返回ResultSets的方法。這些方法需要返回ResultSet的封裝類。為了保證ResultSetWrapper的一致性,我們在StatementWrapper中添加了一個傳遞給其構建器的lastSqlString實例變量。當我們對特定的SQL語句進行性能監測時,該實例變量就非常有用了。返回ResultsSets的方法如下所示:
//StatementWrapper方法 public ResultSet executeQuery(String sql) throws SQLException { //PreparedStatementWrapper方法 |
第三,一些方法使用了java.sql.Array對象。由于這些Array對象能夠返回ResultSet,因此我們再次需要提供一個Array封裝對象,以便返回ResultSetWrapper而不是普通的ResultSets。另外,我們還需要處理Array對象被傳遞給setArray()方法的情況:如果傳遞的是Array封裝對象,則在被傳遞給PreparedStatement前,該對象需要被解封裝:
public void setArray(int i, Array x) throws SQLException { public Array getArray(int i) throws SQLException { |
最后,我們創建這些封裝類的目的是能夠實現性能的監測。我們要在下面的方法中添加測試JDBCLogger類性能的功能,這樣每個方法都有一個對被封裝在測試調用中的真正執行方法的調用。我們將sql字符串和當前的線程傳遞給測試調用,因為對于任何類型的測試調用來說,這二個參數都是十分重要的,尤其是在測量過程運行的時間時更是如此。另外,需要注意的是,我還重新定義了返回ResultSets的executeQuery()方法,以便在其中插入測試類:
//StatementWrapper方法 public boolean execute(String sql) throws SQLException { public int[] executeBatch() throws SQLException { public ResultSet executeQuery(String sql) throws SQLException { public int executeUpdate(String sql) throws SQLException {
public ResultSet executeQuery() throws SQLException { public int executeUpdate() throws SQLException { |
封裝ResultSet類
ResultSetWrapper類也主要包括托付方法:
public ResultSetWrapper(ResultSet resultSet, StatementWrapper statement, String sql) { public boolean absolute(int row) throws SQLException { ... |
其中也有一些方法不是簡單的托付方法,getStatement()方法返回生成ResultSet的statement對象,我們需要讓它返回StatementWrapper對象:
public Statement getStatement() throws SQLException { public Array getArray(int i) throws SQLException { public Array getArray(String colName) throws SQLException { |
最后,我們需要添加測試過程。許多開發人員都錯誤地認為,不同的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 } |
一個更有用的定義是測試查詢的時間。下面的方法記錄查詢開始時的時間,并在查詢結束時得出使用的時間。由于假定在同一個線程中SQL查詢不能遞歸(一般情況下都是這樣的),下面的方法是相當簡單的:
private static Hashtable QueryTime = new Hashtable(); public static void startLogSqlQuery(Thread t, String sql) public static void endLogSqlQuery(Thread t, String sql) |
使用JDBCLogger類中的這些方法的輸出將如下所示:
Time: 53 millis for SQL query SELECT * FROM JACKTABL
對于每次查詢執行來說,這將使我們能夠精確地測試SQL查詢所使用的時間,也能夠計算出JDBCLogger類中所有查詢所需要的時間。我經常測試的是最小、最大、平均、平均偏差等值,這些值在測試大規模的系統的性能時更有用。
使用JDBC封裝類框架
我們已經介紹了非常有用的在應用程序的開發和布置階段測試JDBC調用性能的方法。由于封裝類比較簡單,而且功能強大,又不需要對應用程序進行大量的修改,它們可以被保留在已經布置好的應用程序中,創建一個可配置的JDBCLogger類將使我們能夠根據自己的需要開啟或關閉測試功能。
在開發階段,由于能夠計算出累積的時間代價,我們能夠利用這些類辨別出個別的需要較大時間代價的數據庫交互過程和重復的數據庫交互過程,哪個的時間代價更大。辨別出時間代價較大的數據庫交互過程是我們改進應用程序性能的第一步。在開發階段,這些封裝類可以用來發現應用程序的理論性能和實際性能之間的差距,有助于我們分析為什么會有差距。
在利用這些類找出JDBC的性能瓶頸在哪里后,我們就可以對數據庫的接口進行調整了。我將在以后的文章中繼續討論JDBC性能的技術。