提升數據訪問層的性能

提升數據訪問層的性能
在J2EE應用中,我們經常通過JDBC訪問企業資源。但JDBC用的不好,將會影響系統的性能。本文參照John Goodson的《Performance Tips for the Data Tier(JDBC)》一文,寫成此文,希望對我們的開發有所幫助。
本文從以下四個部分加以說明:
l    適當地使用數據庫的元數據方法
l    檢索需要的數據
l    選擇優化性能的功能
l    管理連接和數據更新
1.    適當地使用數據庫的元數據方法
1.1.    儘量少用元數據方法
由於元數據方法執行速度比較慢,故要儘量少用元數據方法。由於調用元數據方法產生結果集需要大量的開銷,由元數據方法產生的結果集應該緩存起來,而不是多次執行查詢,這樣可以提供JDBC的性能。例如在應用中你調用了getTypeInfo一次,你就應該將結果集緩存起來,共應用再次使用。
1.2.    避免查詢模式
給元數據提供null參數或查詢模式將會產生耗時的查詢。同時,由於一些不需要的數據通過網絡傳遞,導致網絡流量的增大,降低整個系統的性能。由於元數據方法執行比較慢,所以儘可能地給它提供非null參數和高效地調用它。而我們的應用常出現這樣的現象:
ResultSet WSrs = WSc.getTables (null, null, "WSTable", null);
應該改成:
ResultSet WSrs = WSc.getTables ("cat1", "johng", "WSTable",  "TABLE");
顯然,在第一個getTables()調用中,應用可能需要知道WSTable表是否存在。當然, JDBC驅動按字面上的調用與解析請求不同。JDBC是這樣解析請求的:返回所有的名稱叫“WSTable”的表,視圖,系統表,同義詞,零時表,或在任何數據庫目錄中數據庫的模式存在的別名。
第二個getTables()的調用更準確地反映了應用需要知道什麼。JDBC這樣解析這個請求:返回所有名叫“WSTable”存在與當前目錄中模式爲“johng’的所有表。顯然,JDBC驅動處理第二個請求要比處理第一個請求來得更有效。
給元數據方法提供的信息越多,你得到的信息的準確性和性能也越高。
1.3.    使用啞元查詢來確定表的特徵
避免使用getColumns()確定一個表的特徵。用getMedata()啞元查詢替換之。考慮一個容許用戶選擇列的應用。應用應該用getColumns()返回用戶列的信息還是準備一個啞元查詢並調用getMetadata()呢?
情形1:getColumns方法
ResultSet WSrc = WSc.getColumns (... "UnknownTable" ...);// This call to getColumns() will generate a query to// the system catalogs... possibly a join// which must be prepared, executed, and produce// a result set. . .WSrc.next();string Cname = getString(4);. . .// user must retrieve N rows from the server// N = # result columns of UnknownTable// result column information has now been obtained
情形2:getMetadata方法
// prepare dummy queryPreparedStatement WSps = WSc.prepareStatement  ("SELECT * from UnknownTable WHERE 1 = 0");// query is never executed on the server - only preparedResultSetMetaData WSsmd=WSps.getMetaData();int numcols = WSrsmd.getColumnCount();...int ctype = WSrsmd.getColumnType(n)...// result column information has now been obtained
在兩個情形中,查詢被送到服務器上。但在情形1中,查詢必須被準備和執行,結果描述信息必須被簡潔地表達,並且結果集必須送到客戶端。在情形2中,一個簡單的查詢必須準備並且僅有結果描述信息被簡潔地描述。顯然,情形2是更好的性能模式。
這多少有些把這個討論複雜化了,讓我們考慮一個不支持本地準備SQL語句的數據庫。情形1的性能沒有變,但由於啞元查詢必須被求值而不是僅僅準備,因此情形2的性能稍微有些增加。因爲查詢語句的Where子句計算結果總是FALSE,因此查詢沒有產出結果行和不存取表數據的執行。在這個情形下,方法2仍然要比方法1做的好。
總之,總是使用結果集元數據檢索表列信息,如列名,列數據類型和列精度和數值範圍。當被請求的信息不能從結果記錄集(例如,表列默認值)獲取的時候,僅僅使用getColumns()方法。
2.    檢索需要的數據
2.1.    檢索長數據
除非必要,由於檢索長數據會造成網絡資源緊張而降低性能。通常大多數用戶不需要看到長數據,如果用戶需要看這些數據,應用再去檢索。
我們的代碼中長出現這樣的代碼:select * from <table name> …如果選擇的表中有長數據列,那這個查詢的性能將會非常糟糕。再說,表中的所有數據項你都需要嗎?如果不需要,爲什麼要讓它們在網絡上傳遞,浪費網絡資源?
例如,看看下邊的JDBC代碼:
ResultSet rs = stmt.executeQuery (   "select * from Employees where SSID = '999-99-2222'");rs.next();string name = rs.getString (4);
JDBC不是智能的。當你這樣寫代碼的時候,它根本就不知道你真正需要那些列,它把所有的都返回當然是情理之中的事情了,所以開發的時候就勞煩把需要的列在Select語句中指明。如果Employees表中有照片之類的長數據字段,系統的性能之低就可想而知了。
儘管有方法getClob()和getBlod()支持這種長數據字段的檢索,但並不是每個數據庫都支持它。所以記住:需要長數據的時候再去讀它。
2.2.    減少檢索到的數據的大小
有時候,長數據必須被檢索。在這種情況下,大多數用戶可能不需要在屏幕看到100k(或更多)的正文。 爲了減少網絡流量和提高性能,你可以通過調用setMaxRows(),setMaxFieldSize(),以及與驅動相關的setFetchSize()方法把檢索到的數據大小減少到可管理的範圍之內。另一個減少檢索到的數據大小的方法是減少列的數量。如果驅動允許你定義包尺寸,使用最小的包尺寸將會滿足你的需要。
記住:注意只返回你需要的行和列。如果你返回了五列而你只需要兩列,性能就降低了??特別是不需要的結果中包含了長數據。
2.3.    選擇正確的數據類型
檢索和送出某種數據的類型的開銷是很昂貴的。當設計數據庫模式時,選擇能最有效處理的數據類型。例如,整型要比浮點數和小數數據要快。浮點數根據數據庫特殊的格式定義,通常是壓縮格式。爲了能被數據庫通訊協議處理,這些數據必須被解壓後再轉換成不同的格式。
2.4.    檢索記錄集
由於數據庫系統對滾動遊標的有限支持,大多數JDBC驅動不能實現滾動遊標。除非你確定數據庫支持滾動記錄集(例如,rs),否則不要調用rs.last()和rs.getRow()去得到記錄集有多少行。對模仿滾動遊標的JDBC驅動而言,調用rs.last()會導致驅動爲了到最後一行而通過網絡檢索所有的數據。可以替代的方法是你可以通過記錄集枚舉記錄行數,或者通過提交在SELECT語句中一個帶有COUNT列的查詢得到行數。
一般情況下,不要寫依賴於記錄集行數的代碼,因爲爲了取得行數,驅動必須讀取記錄集中的所有的行。

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來找出最小的唯一索引。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章