在前面的幾節中,我們經常會提到一個詞“Fetchsize”,並且在《JBOSS連接池調優2-合理的設置PreparedStatementCache》一節中研究了fetchsize對應用內存的影響。網上關於fetchsize的文章不是很多,很難對這個參數有一個全面的瞭解,確實,如果從開發的角度來理解fetchsize,確實是存在一些困難的。那麼到時什麼是fetchsize?fetchsize對內存和性能到底有什麼影響呢?我在前段日子作了不少測試和研究,下面將自己的研究成果和大家分享一下。
1. 什麼是fetchsize?
1.1 Oracle中的fetchsize
先來簡單解釋一下,當我們執行一個SQL查詢語句的時候,需要在客戶端和服務器端都打開一個遊標,並且分別申請一塊內存空間,作爲存放查詢的數據的一個緩衝區。這塊內存區,存放多少條數據就由fetchsize來決定,同時每次網絡包會傳送fetchsize條記錄到客戶端。應該很容易理解,如果fetchsize設置爲20,當我們從服務器端查詢數據往客戶端傳送時,每次可以傳送20條數據,但是兩端分別需要20條數據的內存空閒來保存這些數據。fetchsize決定了每批次可以傳輸的記錄條數,但同時,也決定了內存的大小。這塊內存,在oracle服務器端是動態分配的(大家可以想想爲什麼)。而在客戶端(JBOSS),PS對象會存在一個緩衝中(LRU鏈表),也就是說,這塊內存是事先配好的,應用端內存的分配在conn.prepareStatement(sql)或都conn.CreateStatement(sql)的時候完成。
在java程序中,我們會執行以下代碼:
//打開遊標,執行查詢,但是並不獲取任何的數據,網絡上沒有數據的傳輸。 rs = stmt.executeQuery(); //獲取具體的數據,網絡一般每次傳輸fetchsize條數據。 while (rs.next()){ }
1.1 MYSQL中的fetchsize
MYSQL的preparestament基本上不佔用內存,爲什麼呢?因爲MYSQL並不需要象oracle那樣的一塊內存來保存結果集緩衝區,爲什麼不需要緩衝區,其中根本的原因是由MYSQL的通訊方式決定的。
MYSQL 客戶端/服務器協議是半雙工的,即MYSQL只能在給定的時間,發送或接受數據,但不能同時發送和接收。所以,MYSQL在數據查詢結果集傳送的時候,需要一次性將數據全部傳送到客戶端,在客戶數據接收完之後,釋放相關的鎖等資源。因爲這種半雙工的通訊方式,所以MYSQL不需要客戶端的遊標,但是客戶端API通過把結果取到內存中,可以模擬遊標的操作。所以,我們可以在JAVA程序中,可以象ORACLE那樣來實現MYSQL的訪問。
2. 如何設置fetchsize?
Fetchsize可以在任何一層進行設置 ,ORACLE JDBC驅動默認的FETCHSIZE爲10。一般爲了方便,我們會在數據源層面上來設置fetchsize。
2.1 語句級別的設置:
我們可以在jdbc中調用Preparedstatement .setFetchSize()的進行設置:
stmt = conn.prepareStatement(sql); stmt.setFetchSize(50);
也可以在Ibatis, hibernate等框架上直接針對某個語句進行設置:
< select id="getAllProduct"> select * from employee < /select>
2.2 數據源中的全局設置
JBOSS連接中設置:
< connection-property name="defaultRowPrefetch">50</ connection-property>
2.3 Fetchsize的核心源碼:
可以在JDBC驅動類Oracle.jdbc.driver.OracleStatment中找到這個方法, setPrefetchInternal方法中傳入的默認值爲0,僞代碼如下:
void setPrefetchInternal(int paramInt){ if (paramInt < 0) { DatabaseError.throwSqlException(68, "setFetchSize"); } //獲取連接池中的DefaultRowPrefetch屬性 else if (paramInt == 0) { paramInt = this.connection.getDefaultRowPrefetch(); } if (paramInt == this.defaultRowPrefetch) return; this.defaultRowPrefetch = paramInt; if ((this.currentResultSet == null) || (this.currentResultSet.closed)) { this.rowPrefetchChanged = true; } }
3. Fetchsize對性能影響的測試:
3.1 空查詢結果集的測試:
查詢的表一共有300條記錄,測試中查詢的結果集爲空,執行的是全表掃描。
SQL> select count(*) from test10000; COUNT(*) ---------- 300 SQL> select * from test10000 where col_a='test'; no rows selected
數據庫 | 連接方式 | PSCACHE | fetchsize | 字段長度 | 網絡距離 | 總記錄數 | 返回記錄 | 執行時間 (ms) |
ORACLE | oci | 支持 | 1 | 10000 | 15KM | 300 | 0 | 1.5875 |
ORACLE | oci | 支持 | 5 | 10000 | 15KM | 300 | 0 | 1.5828 |
ORACLE | oci | 支持 | 10 | 10000 | 15KM | 300 | 0 | 1.7781 |
ORACLE | oci | 支持 | 50 | 10000 | 15KM | 300 | 0 | 2.0468 |
ORACLE | oci | 支持 | 100 | 10000 | 15KM | 300 | 0 | 2.6656 |
ORACLE | oci | 支持 | 1 | 10000 | 本地 | 300 | 0 | 0.1646 |
ORACLE | oci | 支持 | 5 | 10000 | 本地 | 300 | 0 | 0.1713 |
ORACLE | oci | 支持 | 10 | 10000 | 本地 | 300 | 0 | 0.1898 |
ORACLE | oci | 支持 | 50 | 10000 | 本地 | 300 | 0 | 0.3431 |
ORACLE | oci | 支持 | 100 | 10000 | 本地 | 300 | 0 | 1.2609 |
ORACLE | thin | 支持 | 1 | 10000 | 15KM | 300 | 0 | 1.6344 |
ORACLE | thin | 支持 | 10 | 10000 | 15KM | 300 | 0 | 1.6687 |
ORACLE | thin | 支持 | 100 | 10000 | 15KM | 300 | 0 | 1.6266 |
MYSQL | jdbc | 支持 | 1 | 10000 | 15KM | 300 | 0 | 1.5187 |
MYSQL | jdbc | 支持 | 10 | 10000 | 15KM | 300 | 0 | 1.6093 |
MYSQL | jdbc | 支持 | 100 | 10000 | 15KM | 300 | 0 | 1.5906 |
從上面的測試中,可以得出如下結論:
- 在沒有記錄返回的情況下,OCI方式中fetchsize設置越大,對性能的影響越大。
- 在沒有記錄返回的情況下,THIN和mysql的方式中,fetchsize的大小,對於性能影響不大。
這是在空結果集情況下的影響,僅供參考,不應該作爲我們考慮的情況。
3.2 非空查詢結果集的測試:
數據庫 | 連接方式 | PSCACHE | fetchsize | 字段長度 | 網絡距離 | 總記錄數 | 返回記錄 | 執行時間 (ms) |
ORACLE | oci | 支持 | 1 | 100 | 15KM | 300 | 300 | 226.9533 |
ORACLE | oci | 支持 | 5 | 100 | 15KM | 300 | 300 | 86.44667 |
ORACLE | oci | 支持 | 10 | 100 | 15KM | 300 | 300 | 43.74667 |
ORACLE | oci | 支持 | 50 | 100 | 15KM | 300 | 300 | 10 |
ORACLE | oci | 支持 | 100 | 100 | 15KM | 300 | 300 | 5.2 |
ORACLE | oci | 支持 | 1 | 100 | 15KM | 300 | 10 | 8.44 |
ORACLE | oci | 支持 | 5 | 100 | 15KM | 300 | 10 | 2.9 |
ORACLE | oci | 支持 | 10 | 100 | 15KM | 300 | 10 | 1.56 |
ORACLE | oci | 支持 | 50 | 100 | 15KM | 300 | 10 | 1.56 |
ORACLE | oci | 支持 | 1 | 100 | 本地 | 300 | 300 | 12.773 |
ORACLE | oci | 支持 | 5 | 100 | 本地 | 300 | 300 | 5.32 |
ORACLE | oci | 支持 | 10 | 100 | 本地 | 300 | 300 | 2.9 |
從上面的測試中,可以得出如下結論:
- 當返回結果集較大時,設置較大的fetchsize,對性能會有很大的提升。
- Fetchsize設置大於返回的記錄數時,對於性能的提升沒有任何的意義,反而會增加內存的開銷。
3.3 Fetchsize對性能和內存的影響,下面的圖可以很好的說明他們的關係:
3.4 Fetchsize和網絡的關係
當fetchsize設置到某一值時,便不會再有性能的提升,這不僅僅是因爲結果集大小的原因,和操作系統或者ORACLE上的TCP讀/寫的緩衝區也有關係:
–操作系統上控制網絡的讀/寫 buffer – net.core.rmem_default = 262144 – net.core.wmem_default = 262144 –數據庫端控制,默認值爲操作系統上的設置 : – RECV_BUF_SIZE=9375000 - SEND_BUF_SIZE=9375000
4. 總結:
fetchsize的設置,跟具體業務系統有關係,沒有一個最好的值可以供各個應用都可以使用。一般OLTP的系統,fetchsize使用jdbc的默認值就可以了。我查看了下網絡上的大部分文章,在某個特定的條件下測試的fetchsize,得出一個值,然後所有人都用這個值來設置自己的應用系統。一般情況下,這僅僅只是一些資源的浪費,但是,在某些情況下,如數據源拆分,讀寫分離架構中,當fetchsize設置的太大,有可能會導致性能的急劇下降,甚至會導致應用上可怕的JVM內存溢出,在不少公司發生過這種慘痛的教訓。建議在設置這個值之前,先做一個JVM內存的DUMP,以便能夠對內存的佔用情況有一個清晰的瞭解。