本文由 ImportNew - 劉志軍 翻譯自 JAVAranch。歡迎加入JAVA小組。轉載請參見文章末尾的要求。

注:為了更好理解本文,請結合原文閱讀

 

在上一篇文章中提到了PreparedStatement的局限性:PreparedStatement不允許一個預留位置(?)設置多個值,本文試圖從其它角度來解決該問題。

 

在網路上開銷最昂貴的資源就是用戶端與伺服器往返的請求與回應,JDBC中類似的一種情況就是對資料庫的調用,如果你在做資料插入、更新、刪除操作,可以使用executeBatch()方法減少資料庫調用次數,如:

 

Statement pstmt = conn.createStatement();
pstmt.addBatch("insert into settings values(3,'liu')");
pstmt.addBatch("insert into settings values(4,'zhi')");
pstmt.addBatch("insert into settings values(5,'jun')");
pstmt.executeBatch();
但不幸的是對於批量查詢,JDBC並沒有內建(built-in)的方法,而且JDBC執行批次處理的時候也不能有SELECT語句,如:

 

Statement pstmt = conn.createStatement();
pstmt.addBatch("select * from settings");
pstmt.executeBatch();
會拋出異常:

 

Exception in thread "main" java.sql.BatchUpdateException: Can not issue SELECT via executeUpdate().
at com.mysql.jdbc.Statement.executeBatch(Statement.java:961)
at test.SelectBatchTest.test2(SelectBatchTest.java:49)
at test.SelectBatchTest.main(SelectBatchTest.java:12)
假設你想從一系列指定的id清單中獲取名字,邏輯上,我們要做的事情看起來應該是:

 

 
PreparedStatement stmt = conn.prepareStatement(
"select id, name from users where id in (?)");
stmt.setString("1,2,3");
但是這樣做並不能得到預期的結果,JDBC只允許你用單個的字面值來替換「?」 JDBC之所以這麼做是有必要的,因為如果SQL自身可以改變的話,JDBC驅動就沒法預編譯SQL語句了,另一方面它還能防止SQL注入攻擊。

 

但有四種可替代的實現方法可供選擇:

 

分別對每個id做查詢
一個查詢做完所有事
使用預存程序
分批次處理
方法一: 分別對每個id做查詢

 

假設有100個id,那麼就有100次資料庫調用:

 

PreparedStatement stmt = conn.prepareStatement(
"select id, name from users where id = ?");
for ( int i=0; i < 100; i++ ) {
stmt.setInt(i); // or whatever values you are trying to query by

 

// execute statement and get result
}
這種方法寫起來非常簡單,但是性能非常慢,資料庫往返要處理100次。

 

方法二:一個查詢完成所有事

 

在運行時,你可以使用一個迴圈來構建如下SQL語句:

 

PreparedStatement stmt = conn.prepareStatement(
"select id, name from users where id in (?, ?, ?)");
stmt.setInt(1);
stmt.setInt(2);
stmt.setInt(3);
這種方案從代碼相比第一種方法算是第二簡單的,它解決了來回多次請求資料庫的問題,但是如果每次請求參數的個數不一樣時預處理語句就必須重新編譯,由於每次SQL字面值不匹配,因此如果分別用10個id、3個id、100個,這樣會在緩存中產生三個預處理語句。除了重新編譯預處理語句之外,先前緩存池中的預處理語句將被移除(受限於緩存池大小),進而導致重新編譯已編譯過的語句。最後,這種查詢方式在記憶體溢出或磁片分頁操作時查詢會佔用很長時間。

 

該方案的另一種變體就是在SQL語句中硬編碼:

 

PreparedStatement stmt = conn.prepareStatement(
"select id, name from users where id in (1, 2, 3)");
這樣方式甚至更差,而且沒有任何機會對SQL語句重用,至少用「?」還可以對使用相同數量參數的SQL語句進行重用。

 

PreparedStatement stmt = conn.prepareStatement(
"select id, name from users where id in (?) ; "
+ "select id, name from users where id in (?); "
+ "select id, name from users where id in (?)");
stmt.setInt(1);
stmt.setInt(2);
stmt.setInt(3);
這種方法的優點就是每次查詢模版語句都一樣,資料庫不需要每次計算執行路徑。然而,從資料庫驅動的角度來說SQL每次都不一樣,預處理語句每次必須預處理保存在緩存中。而且不是所有資料庫系統都支援分號間隔的多個SQL語句的

 

方法三:使用預存程序

 

預存程序執行在資料庫系統中,因此它可以做很多查詢而不需要太多網路負載,預存程序可以收集所有結果一次性返回。這是一種速度很快的解決方案。但是它對資料庫的依賴比較強,不能隨意的切換資料庫系統,否則需要重寫預存程序而且需要你分離應用伺服器與資料庫伺服器之間的邏輯。如果應用架構已經使用了預存程序,無疑這是只最佳方案。

 

方法四:分批次處理

 

批量查詢是方案一和方案二的折衷選擇,它預先確定一批查詢參數的常量,然後用這些參數構建一批查詢。因為這只會涉及到有限個查詢,所以它有預處理語句的優勢(預編譯不會與緩存中的預處理發生碰撞)。批次處理多個值在相同的查詢保留了伺服器來回請求最小化的優勢。最後你可以通過控制批次處理的上限來避免大查詢的記憶體問題。如果你有很關鍵的查詢對性能方面有要求又不想用預存程序,那麼這是一種很好的解決辦法,現在我們通過一個例子說明:

 

public static final int SINGLE_BATCH = 1;
public static final int SMALL_BATCH = 4;
public static final int MEDIUM_BATCH = 11;
public static final int LARGE_BATCH = 51;
第一件要做的事是你要衡量有多少批次處理以及每個批次處理的大小。(注意:在真實的代碼中,這些值應該寫在一個設定檔中而不是採取硬式編碼形式,也就是說,你可以在運行時試驗並改變批次處理的大小)不管真正的批次處理大小是多大,你總需要一個單個的批次處理—大小為1的批次處理(SINGLE_BATTCH)。這樣如果有人請求的就是一個值或者在一個很大的查詢中最後有遺留下來的單個值都能派上用場。對於批次處理的大小,使用素數會更好些。換句話說,大小不應該可以相互的整除或者被相同的數整除。請求數的最大值將有最少的伺服器往返。批次處理的大小的數量和真正的大小是基於配置變化的。需要注意的是:大的批次處理大小不應該太大否則你將遇到記憶體麻煩。同時最小批次處理的大小應該很小,你可能會使用這個來做很多次的查詢。

 

while ( totalNumberOfValuesLeftToBatch > 0 ) {
按如下方式重複操作直到推出迴圈。

 

int batchSize = SINGLE_BATCH;
if ( totalNumberOfValuesLeftToBatch >= LARGE_BATCH ) {
batchSize = LARGE_BATCH;
} else if ( totalNumberOfValuesLeftToBatch >= MEDIUM_BATCH ) {
batchSize = MEDIUM_BATCH;
} else if ( totalNumberOfValuesLeftToBatch >= SMALL_BATCH ) {
batchSize = SMALL_BATCH;
}
totalNumberOfValuesLeftToBatch -= batchSize;
這種方案在這裡是查找到最大的批次處理大小,可能這個最大值比我們實際要查詢的值稍大。舉例說明:假設查詢有75個參數,那麼首先選擇51個元素(LARGE_BATCH),現在還剩24個待查詢,然後接著用11個元素的查詢(MEDIUM_BATCH)。現在還有13個值,因為仍然大於11,再做一次11個元素的查詢,現在只剩下2個值,它少於那個最小的批次處理4(SMALL_BATCH),所以做兩次單查詢。總共5次往返用了3次預處理在緩存中。這是一個很重要的改進比單獨地坐75次單查詢。

 

StringBuilder inClause = new StringBuilder();
boolean firstValue = true;
for (int i=0; i < batchSize; i++) {
inClause.append('?');
if ( firstValue ) {
firstValue = false;
} else {
inClause.append(',');
}
}
PreparedStatement stmt = conn.prepareStatement(
"select id, name from users where id in (" + inClause.toString() + ')');
現在已經構建了一個真實的預處理語句,由於一直用相同的方式構建的查詢,驅動注意到SQL是相同的。(注意:如果你還沒有用JAVA5,使用StringBuffer替換StringBuilder才能正常編譯),返回id很重要這樣有利於查找哪個名字對應哪個id。

 

 
for (int i=0; i < batchSize; i++) {
stmt.setInt(i); // or whatever values you are trying to query by
}
設置合適的值數量去查詢,包括其他搜尋條件查詢。僅僅只要把這些參數在之舉參數之後。在這種情況你可以最終當前的索引。

 

從這點來看,你僅僅只是執行查詢返回了結果,在第一次嘗試的時候,你應該關注一下性能的提升,根據具體情況調整優化批次處理的大小(batch size)。

 

正如那句名言所說:「過早的優化是萬惡之源」,批次處理應該是用於解決性能問題。
 
原文链接: Javaranch 翻译: ImportNew.com 刘志军
译文链接: http://www.importnew.com/5660.html
转载请保留原文出处、译者和译文链接。]
arrow
arrow
    全站熱搜

    戮克 發表在 痞客邦 留言(0) 人氣()