提高JDBC應用程序的性能

JDBC是Java 數據庫連接 (Java Database Connectivity) API,也是目前Java訪問數據庫的核心部分。這裏,我們不準備太多的去複述JDBC的發展歷史以及它的各種特性。我們嘗試從開發人員關心的性能問題去討論JDBC,同時也介紹幾個提高JDBC性能的基本策略和方法。筆者在自己的數據庫訪問程序中使用了這些方法,從而提升了程序的性能。

: 選擇合適的JDBC 驅動程序模式,並作爲程序設計的考慮因素之一

到目前爲止,JDBC驅動程序有4種模式。選擇何種模式主要取決於程序的應用範圍。正確選擇合適的模式,使之符合於數據庫程序的設計,是提高程序性能必須考慮的一個方面。這裏,我們給出JDBC驅動程序的4種模式的簡要說明:


模式


工作機制

說 明

1

2

JDBC-ODBC

JDBC操作翻譯成對應的ODBC調用。

2

2

本機API/集團式Java驅動

JDBC操作翻譯成針對特定數據庫的調用。

3

3

網絡協議/Java驅動

JDBC操作翻譯成網絡協議並轉發給中間層服務器。

4

2

本級協議/Java驅動

JDBC操作直接轉換成不使用ODBC或本機API的本機協議。

(表 1)



模式

優點

缺點

1

因爲多數RDBMS平臺都支持ODBC 驅動程序,所以使用Jdbc-Odbc 橋能與大量ODBC驅動程序協同工作。

1:用戶受底層ODBC驅動程序的功能限制。

2ODBC需要在每個客戶端得到配置。

3:不能用於applet,因爲applet不能加載本地調用。

2

不需要轉換成Odbc調用,比模式1的性能要好得多。

使用Java本地接口,平臺移植性不好。

3

廣泛適用於Internet/Intranet的開發,安全性和性能都十分顯著。

進行數據庫操作時,需要花費較長的時間。

4

由於是通過本機協議訪問數據庫,所以性能很高。

用戶需要給不同的數據庫使用不同的JDBC驅動程序。

(表 2)

 

建議:當採用模式1時,由於它需要首先把JDBC操作翻譯成對應的ODBC調用,然後這些調用又被傳遞給ODBC驅動程序,最後才執行數據庫的相關操作,很顯然它的性能要大打折扣。所以,在2層結構中,爲了提高程序的性能,我們最好還是用模式2的驅動程序。

說明2:對於進行大型企業開發部署的數據庫開發人員來說採用模式1作爲設計基礎,那簡直就是噩夢。在企業應用中選擇模式3纔是明智的。由於筆者的經驗有限,這裏暫時不對基於模式3的情況進行分析。


小結:關於不同模式驅動程序的選擇,其利弊以及和程序性能三者之間的聯繫,我們這裏就不多說了。雖然模式1有諸多不好,但在後面的分析中我們仍然以模式1作爲我們的實驗環境。

 

:在面向對象的程序設計和提升程序性能之間做出平衡


當我們設計數據庫應用程序的結構時,經常需要把對象模型映射成數據庫中的表,或者把表反向映射成對象模型。面向對象的一個基本原則是:使得每一個對象與真實世界中的對應物具有共同的特徵,以符合我們的思維習慣。


 比如,數據庫中有這樣一張學生表:






Id


Name


Department


Position


Address


1


張三


物理系


團員


昆明


 現在,我們需要把該表從數據庫中映射給用戶,基於上述原則,我們考慮封裝一個類(在這我們暫時就稱它爲Record Set,當然具體到對學生表的映射,指的就是Student類),並且得出一個遵循面向對象設計原則的映射方案:

(圖3――方案1)


我們知道,數據庫應用程序的一個主要功能就是要把從數據庫中查詢到的結果,以某種形式再現給用戶。爲了符合用戶的習慣,我們的程序使用了Java Swing 包中的JTable作爲用戶和程序之間交互的接口。我們現在唯一要做的就是確保程序映射數據的準確與快速.


我們使用AbstractTableModel 關於AbstractTableModel 和 JTable的說明可以查看JDK1.3的Doc作爲映射數據的接口,當然也是用戶與程序交互的核心。我們知道一個表格模型可以抽象爲一個二維對象數組,或者二維Vector這樣的數據結構。於是,我們有了另一個基於數據結構的映射方案:


                                (圖4――方案2)


那究竟選擇哪種方法呢?通過下表的比較,我們不難得出結論。

                                   (表 4)


說明1方案1表面上似乎遵循了面向對象的原則,但是:


(1):其工作量顯然比方案2大。


(2):結果集因爲類封裝而失去了靈活性。


(3):中間的各種轉化會使程序的性能降低。


說明2但是方案1 在很多情況下都是最佳的選擇。因爲,當用戶通過Web方式與程序交互時,實現對結果集的再封裝就是必要的了。


小結在筆者的程序中,爲了提高程序的性能,不得以放棄了面向對象的程序設計原則。而以JTableAbstractTableModel API的本身特性反向得出了映射方案。有時依賴於API提供的方法,往往能提高程序的性能。當然這樣的選擇,主要取決於實際的要求和整個系統的結構設計,我們應該從不同的角度去考慮,最後才能做出平衡。
 
 
: 優化數據庫連接
在優化Java編寫的程序時,對象重用是一個經常使用的方法。其中關鍵的一點,就是要儘量的重用一個已有的對象,而不是反覆的創建一個新的對象。這樣不僅能減少對內存的消耗,也降低了程序因爲不斷創建新對象而導致內存溢出的情況。由於建立一個數據庫連接或者撤銷一個連接都是代價昂貴的操作,所以重用一個Connection就是理所當然的了。我們有兩種方式重用一個Connection:
 
方法
方法說明
使用環境
1
爲每個用戶建立一個單獨的連接,並反覆使用該連接創建語句,執行相關的數據庫操作。
每個用戶必須經過驗證才能使用該連接。
2
建立連接池
多用戶
(表 5)
我們使用完一個連接後一定要將其關閉,否則可能會因爲連接資源耗盡,而導致程序性能降低,甚至使程序崩潰。
   關閉聯接有以下四種方式:
(1):不關閉連接
    (2): // ……
Connection con = DriverManager.getConnection(url,user,password);
con = null;
       // ……
    (3): // ……
Connection con = DriverManager.getConnection(url,user,password);
con.close();//顯示的關閉連接
      // ……
    (4):// ……
Connection con = DriverManager.getConnection(url,user,password);
con.close();//顯示的關閉連接
con = null;
       // ……
 
下面我們給出一些測試數據,通過比較我們不難得出關閉連接的正確方式:
 首先我們給出測試程序,我們創建100個線程,每個線程都各自獲得1個數據庫連接。這裏的每個線程就好比一個希望連接數據庫的用戶。
 
TestJdbc.java
import java.sql.Connection;
import java.sql.DriverManager;
public class TestJdbc {
    public TestJdbc() {           
    }
    public void connect(){
      try{
          Class.forName("sun.jdbc.odbc.JdbcOdbcDriver").newInstance();
     }
     catch(Exception ex){
        ex.printStackTrace();
    }
       //創建100個線程
      for(int i=0;i<100;i++){
        MakeConnection makeConnect = new MakeConnection();
        makeConnect.setName(""+i+"");
        makeConnect.start();
      }
    }
   
   public static void main(String args[]){    
     TestJdbc test = new TestJdbc();
     test.connect();       
  }
   
class MakeConnection extends Thread {
   public MakeConnection () {
    }
    //創建1個連接
    public void run(){
        try  {    Connection con =null;
                  //關閉連接的方式,可以是上述4種方式之一
                  con = DriverManager.getConnection("jdbc:odbc:test");
                  con.close();
                  con = null;
        }  
       catch(Exception e){
            e.printStackTrace();
       }
    }
  }
 測試一:Java堆內存分配最小爲0Kb,最大爲2000Kb
 
 
方式1
方式2
方式3
方式4
預期創建連接數目()
100
100
100
100
創建及關閉連接耗時(毫秒)
6,540
7,850
20,320
13,950
實際生成連接實例數()
100
100
100
100
剩餘連接實例數 ()
26
24
24
23
創建連接消耗內存量(字節)
8,800
8,800
8,800
8,718
剩餘連接佔用內存量(字節)
2,288
2,112
2,112
2,024
                                      (表 6)
    從測試一的數據我們看不出四種方式有什麼太大的差別,這是因爲Java堆分配的太小,當程序的內存消耗過大時,垃圾回收線程就自動回收那些無用的對象實例,從而影響了我們的測試結果。我們嘗試分配一個更大的Java堆,以排除垃圾回收機制對測試結果的影響。
測試二:Java堆內存分配最小爲0Kb,最大爲20,000Kb
 
方式1
方式2
方式3
方式4
預期創建連接數目()
100
100
100
100
創建及關閉連接耗時(毫秒)
6,650
6,430
9,280
9,610
實際生成連接實例數()
63
65
100
100
剩餘連接實例數 ()
63
65
100
100
創建連接消耗內存量(字節)
5,544
5,720
8,800
8,800
剩餘連接佔用內存量(字節)
5,544
5,720
8,800
8,800
(表 7)
 
說明:(1)剩餘連接實例數是指沒有被垃圾回收的連接數。     
(2) (7)剩餘連接實例數與實際生成連接實例數相同,說明在程序運行期間都沒有進行垃圾回收。
     從(表7)中我們看出方式1和方式2實際生成連接數小於預期創建連接數目,可能是因爲沒有顯示的關閉連接造成的。考慮到垃圾回收的不確定性,我們最好採用方式3或者方式4來顯示的關閉連接,這樣做是最可靠的方法。
 
 
四:使用預編譯語句和批量更新
首先我們得大致的瞭解數據庫是怎麼處理各種數據庫操作語句的。當數據庫接收到一個語句時,數據庫引擎首先解析該語句,然後分析是否有語法,語義錯誤。如果沒有錯誤,數據庫將計算出如何高效的執行該語句。一旦得出執行策略,就由數據庫引擎執行該語句,最後把執行結果反饋給用戶。雖然數據庫廠商對各自的數據庫做了最大的優化,但是可以想象這的確是一個開銷很大的工作。
於是,我們考慮如何使我們的數據庫操作變得更高效呢?如果一條語句執行一次後,數據庫就記錄下該語句的執行策略,那麼以後執行相同語句時,就可以省去上面的種種麻煩了。
Java裏提供了這樣的接口――PreparedStatement.。通過預編譯PreparedStatement 對象, 我們能夠很容易的提高語句執行效率。同時,需要指出的是Java裏還有另一個實現數據庫操作的接口――Statement,但是當語句格式固定時我們更傾向於使用PreparedStatement,只有當語句格式無法預見時,我們才考慮採用Statement。
以下是執行1000次語句結構相同的Insert,Update和Select語句的測試結果:
 
接口類型
Insert語句
Update語句
Select語句
第一次測試耗時
第二次測試耗時
第一次測試耗時
第二次測試耗時
第一次測試耗時
第二次測試耗時
Statement
2360 ms
2190 ms
3790 ms
3460 ms
3570 ms
2530 ms
PreparedStatement
1270 ms
1040 ms
2600 ms
2410 ms
440 ms
380 ms
                                     (表8)
分析: PreparedStatement的效率明顯比Statement要高出很多。另外,對於查詢語句我們還得深入地看看JDBC是如何實現的。JDBC執行一次查詢後,將返回一個ResultSet(結果集)。爲了建立這個結果集,JDBC將對數據庫訪問兩次。第一次要求數據庫對結果集中的各列進行說明,第二次告訴數據庫,當程序需要獲取數據時應如何安置這些數據。由此我們能夠算出執行一次或多次查詢,JDBC需要訪問數據庫的次數。
訪問數據庫次數 = 結果集中的列數 * 語句執行的次數 * 2
 
如果同樣執行100次相同查詢,結果集中的列數也相同時,假設爲20列:
使用Statement:  訪問數據庫次數 = 20 * 100 * 2 = 4000   
使用Prepared Statement:  訪問數據庫次數 = 20 * 1* 2 = 400
 
 
以下是相關的測試結果:

方式
Select語句
執行100次語句結構相同的查詢耗時
執行1000次語句結構相同的查詢耗時
第一次測試
第二次測試
第一次測試
第二次測試
方式1
1100 ms
330 ms
3510 ms
3020 ms
方式2
110 ms
50 ms
440 ms
380 ms
                                   (表9)
分析:測試結果說明,如果不正確的使用了PreparedStatement接口,那麼其執行效率和使用Statement沒有什麼差別,而PreparedStatement接口的優勢也不會得到充分發揮。
最後我們還得補充一點,當我們需要成批插入或者更新記錄時。我們考慮採用Java的批量更新機制,這一機制允許多條語句一次性提交給數據庫批量處理。通常情況下比單獨提交處理更有效率。如果我們再配合使用PreparedStatement接口,將進一步提高程序的性能。我們同樣給出一個小程序予以說明。
五:通過對象引用,重用結果集

    當程序開發人員爲提高程序性能而絞盡腦汁時,我們首先得想一想在程序中是否使用了緩衝技術。在大量的基於C/S模式的應用程序中,我們可以從兩個不同的角度使用它。從客戶端的角度,我們可以把數據緩衝到本地機,從而避免進行相同的網絡操作;從服務器端的角度,我們也可以緩衝那些經常使用的數據,從而縮短服務器對相同請求的響應時間。合理地使用緩衝技術能有效的提高程序性能。

   經常出現這樣的情況,在Java編程中一個或多個對象實例在程序運行期間被反覆的使用。更具體的說,在JDBC編程中,用戶經常使用某幾條查詢語句進行查詢,每次執行查詢都返回相同的結果集,這個時候我們就得考慮:那些反覆使用的結果集是否應該被緩衝起來?如何使用這些結果集?用戶更新數據時,如何保證這些結果集也能隨之更新?

這裏,我們將具體的討論JDBC編程中如何實現對結果集的緩衝。首先,我們要澄清如下幾個概念。

(1):在Java編程中如果要使用類,就得把類實例化成對象。類實例化成對象後,就要分配一塊相應的內存空間存放該實例。通過對象引用,可以對這塊內存空間進行操作。

(2):在JDBC中,執行查詢操作返回的結果集,其實是一個指向數據庫的連接,通過遊標操作可以把需要的數據從數據庫調入Java堆。我們用圖表進一步說明:

 (3):對結果集的緩衝實際上是對Java堆中指定的結果集對象進行緩衝,更確切的說應該是緩衝該對象的引用。

基於上述原理,我們決定使用java util 包中的Hashtable實現對結果集對象的緩衝。我們用查詢語句本身作爲該結果集對象的標誌,也就是Hashtable的key字段, value字段則存放了一個Vector對象。此Vector存放了ResultSet的引用“指針”(暫且稱爲指針);Statement或者PreparedStatement的引用“指針”;結果集的記錄數;如果該結果集具有可滾動的特性(JDBC2.0 API的新特性),則還可以存放當前遊標的位置。存放的內容可以根據實際情況調整,但是ResultSet的引用“指針”和 Statement的引用“指針”是必須保存的。具體結構如下圖所示:

 但是,我們發現當內存空間很大,並且需要緩存的對象不是太多時,使用Hashtable的確是最省事的辦法。可是,隨着需緩存的對象數目的不斷增加,Hashtable佔用的內存空間也越來越大。爲了,減少對內存的消耗,同時又能夠實現緩存對象的目的。我們決定設置一個上限,當緩存對象的數目超過該上限時則把最不常用的ResultSet替換出來。下圖是強化了的緩存結構:

 

(注:該數據結構以及LRU算法在jbuilder6.0的Database Pilot代碼包裏有一定介紹。)

當然爲了降低系統消耗,使用的替換算法不能太複雜。另外,可以使用ArrayList和Hashmap來替代Vector和Hashtable,這樣還能提高一點效率。但是ArrayList和Hashmap都是線程不安全的,而Vector和Hashtable卻剛好相反。當我們建立的緩存需要支持多線程訪問時,使用ArrayList和Hashmap就顯得難以控制。所以這裏我們犧牲效率以提高程序的穩定性是有意義的。

最後還得說明一點,當從緩存中刪除一個對象實例時。應按如下步驟進行:

1.         使用close()方法關閉ResultSet(結果集)。

2.         使用close()方法關閉創建該結果集的語句(Statement,PreparedStatement)。

3.         刪除Hasttable中的相應key及其value。

4.         從Vector中刪除key.

 我們還要注意一點,如果使用PreparedStatement接口的方法不當,則不能達到提高執行效率的目的。我們用一個簡單的測試程序說明:
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章