深入瞭解MySQL的流式查詢機制

引言

爲什麼要用流式查詢?

a) 如果有一個很大的查詢結果需要遍歷處理,又不想一次性將結果集裝入客戶端內存,就可以考慮使用流式查詢;

b)分庫分表場景下,單個表的查詢結果集雖然不大,但如果某個查詢跨了多個庫多個表,又要做結果集的合併、排序等動作,依然有可能撐爆內存;詳細研究了sharding-sphere的代碼不難發現,除了group by與order by字段不一樣之外,其他的場景都非常適合使用流式查詢,可以最大限度的降低對客戶端內存的消耗。

 

1、oracle等商業數據庫的fetchsize

使用過oracle數據庫的程序猿都知道,oracle驅動默認設置了fetchsize爲10,那什麼是fetchsize?

先來簡單解釋一下,當我們執行一個SQL查詢語句的時候,需要在客戶端和服務器端都打開一個遊標,並且分別申請一塊內存空間,作爲存放查詢的數據的一個緩衝區。這塊內存區,存放多少條數據就由fetchsize來決定,同時每次網絡包會傳送fetchsize條記錄到客戶端。應該很容易理解,如果fetchsize設置爲20,當我們從服務器端查詢數據往客戶端傳送時,每次可以傳送20條數據,但是兩端分別需要20條數據的內存空閒來保存這些數據。fetchsize決定了每批次可以傳輸的記錄條數,但同時,也決定了內存的大小。這塊內存,在oracle服務器端是動態分配的。而在客戶端,PS對象會存在一個緩衝中(LRU鏈表),也就是說,這塊內存是事先配好的,應用端內存的分配在conn.prepareStatement(sql)或都conn.CreateStatement(sql)的時候完成。

2、流式查詢與MySQL fetchsize的關係

既然fetchsize這麼好用,那MySQL直接設一個值,不就也可以用到緩衝區,不必每次都將全量結果集裝入內存。但是,非常遺憾,MySQL的JDBC驅動本質上並不支持設置fetchsize,不管設置多大的fetchsize,JDBC驅動依然會將select的全部結果都讀取到客戶端後再處理, 這樣的話當select返回的結果集非常大時將會撐爆Client端的內存。

但也不是完全沒辦法,PreparedStatement/Statement的setFetchSize方法設置爲Integer.MIN_VALUE或者使用方法Statement.enableStreamingResults(), 也可以實現流式查詢,在執行ResultSet.next()方法時,會通過數據庫連接一條一條的返回,這樣也不會大量佔用客戶端的內存。

3、MySQL流式查詢的坑

sharding-sphere的執行引擎對數據庫的連接方式提供了兩種:內存限制模式和連接限制模式。(參考:https://shardingsphere.apache.org/document/current/cn/features/sharding/principle/execute/),在內存限制模式中(也就是要使用流式查詢的場景),對於每一張表的查詢,都需要創建一個數據庫連接,如果跨庫跨表查詢操作很多,這對數據庫連接數的消耗將會非常大。起初十分不理解這種方式,爲何不能多個查詢共用同一個連接。一定有什麼我沒有了解清楚的問題。

帶着這個疑問,不妨做一次小小的測試:

使用同一個MySQL數據庫連接,分別執行多次查詢,在得到多個ResultSet之後,再進行結果集的遍歷。

public class LoopConnectionTest {

    private static Connection conn = getConn();

    public static void main(String[] args) {

        List<ResultSet> actualResultSets = new ArrayList<>();

        for (int i = 0; i < 3; i++) {
            actualResultSets.add(getAllCategory(conn));
        }


        boolean flag = true;
        int i = 0;
        while (true) {

            try {
                int index = i++;
                flag = displayResultSet(actualResultSets.get(index%3), index%3);
            } catch (SQLException e) {
                e.printStackTrace();
            }
            if (!flag) {
                break;
            }
        }

    }

    private static ResultSet getAllCategory(Connection conn) {
        String sql = "select * from tb_category";
        PreparedStatement pstmt = null;
        ResultSet resultSet = null;
        try {
            pstmt = (PreparedStatement)conn.prepareStatement(sql);
//            pstmt.setFetchSize(Integer.MIN_VALUE);
            resultSet = pstmt.executeQuery();
        } catch (SQLException e) {
            e.printStackTrace();
        }
//        finally {  
//            if (null!=pstmt) {
//                try {
//                    pstmt.close();//註釋掉close方法是因爲,一旦pstmt關閉,resultSet也會隨之關閉
//                } catch (SQLException e) {
//                    e.printStackTrace();
//                }
//            }
//        }
        return resultSet;
    }

    private static boolean displayResultSet(ResultSet rs, int index) throws SQLException {
        int col = rs.getMetaData().getColumnCount();
        System.out.println("index:" + index + "============================");
        boolean flag = rs.next();
        if (flag) {
            System.out.println(rs.getString("name"));
        }
        return flag;
    }

    public static Connection getConn() {
        String driver = "com.mysql.jdbc.Driver";
        String url = "jdbc:mysql://192.168.178.140:3306/jasper";
        String username = "root";
        String password = "123456";
        Connection conn = null;
        try {
            Class.forName(driver); //classLoader,加載對應驅動
            conn = (Connection) DriverManager.getConnection(url, username, password);
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } catch (SQLException e) {
            e.printStackTrace();
        }
        return conn;
    }

}

第一次試驗,我們將

pstmt.setFetchSize(Integer.MIN_VALUE);

這最關鍵的一行註釋掉,關閉流式查詢,對多個結果集的遍歷可以得到正確的結果。

第二次試驗,開啓流式查詢,果然問題來了。

index:0============================
大 家 電
java.sql.SQLException: Streaming result set com.mysql.jdbc.RowDataDynamic@617f84e0 is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. Ensure that you have called .close() on any active streaming result sets before attempting more queries.
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:935)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:932)
	at com.mysql.jdbc.MysqlIO.checkForOutstandingStreamingData(MysqlIO.java:3338)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2504)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2758)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2820)
	at com.mysql.jdbc.StatementImpl.executeSimpleNonQuery(StatementImpl.java:1657)
	at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:2177)
	at com.cmbc.jdbc.test.LoopConnectionTest.getAllCategory(LoopConnectionTest.java:44)
	at com.cmbc.jdbc.test.LoopConnectionTest.main(LoopConnectionTest.java:16)
java.sql.SQLException: Streaming result set com.mysql.jdbc.RowDataDynamic@617f84e0 is still active. No statements may be issued when any streaming result sets are open and in use on a given connection. Ensure that you have called .close() on any active streaming result sets before attempting more queries.
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:935)
	at com.mysql.jdbc.SQLError.createSQLException(SQLError.java:932)
	at com.mysql.jdbc.MysqlIO.checkForOutstandingStreamingData(MysqlIO.java:3338)
	at com.mysql.jdbc.MysqlIO.sendCommand(MysqlIO.java:2504)
	at com.mysql.jdbc.MysqlIO.sqlQueryDirect(MysqlIO.java:2758)
	at com.mysql.jdbc.ConnectionImpl.execSQL(ConnectionImpl.java:2820)
	at com.mysql.jdbc.StatementImpl.executeSimpleNonQuery(StatementImpl.java:1657)
	at com.mysql.jdbc.PreparedStatement.executeQuery(PreparedStatement.java:2177)
	at com.cmbc.jdbc.test.LoopConnectionTest.getAllCategory(LoopConnectionTest.java:44)
	at com.cmbc.jdbc.test.LoopConnectionTest.main(LoopConnectionTest.java:16)
Exception in thread "main" java.lang.NullPointerException
	at com.cmbc.jdbc.test.LoopConnectionTest.displayResultSet(LoopConnectionTest.java:61)
	at com.cmbc.jdbc.test.LoopConnectionTest.main(LoopConnectionTest.java:26)

查了下異常發生的原因發現,其實mysql本身並沒有FetchSize方法, 它是通過使用CS阻塞方式的網絡流控制實現服務端不會一下發送大量數據到客戶端撐爆客戶端內存,這種實現方式比起商業數據庫Oracle使用客戶端、服務器端緩衝塊暫存查詢結果數據來說,簡直是弱爆了!這樣帶來的問題:如果使用了流式查詢,一個MySQL數據庫連接同一時間只能爲一個ResultSet對象服務,並且如果該ResultSet對象沒有關閉,勢必會影響其他查詢對數據庫連接的使用!此爲大坑,難怪sharding-sphere費勁心思要提供兩種數據庫連接模式,如果應用對數據庫連接的消耗要求嚴苛,那麼流式查詢就不再適合。

貼下MySQL Connector/J 5.1 Developer Guide中原文:

There are some caveats with this approach. You must read all of the rows in the result set (or close it) before you can issue any other queries on the connection, or an exception will be thrown. 也就是說當通過流式查詢獲取一個ResultSet後,在你通過next迭代出所有元素之前或者調用close關閉它之前,你不能使用同一個數據庫連接去發起另外一個查詢,否者拋出異常(第一次調用的正常,第二次的拋出異常)。

 

對比測試了Oracle和DB2,設置fetchSize之後,數據庫連接依然可以被其他查詢共用,並沒有MySQL的這個坑。再一次應證了MySQL相比於大型商業數據庫來說,還是顯得太弱了,這種遊標遍歷的功能理應提供,但是它偏偏沒有。

 

 

 

 

 

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