MySQL JDBC/MyBatis Stream方式讀取SELECT超大結果集

情景: 遍歷並處理一個大表中的全部數據, 這個表中的數據可能會是千萬條或者上億條, 不少人可能會說用分頁limit……但需求自己一次性遍歷更加方便, 且Oracle/DB2都有方便的遊標機制.mysql

  對DB來講Stream其實也就是咱們說的遊標(Cursor), MySQL的Stream方式有2種, Client Side Cursor和Server Side Cursor. JDBC默認的方式Client Side Cursor, 沒有任何設置的默認狀況下JDBC驅動會將select的所有結果都讀取到Client Side後再處理, 這樣的話當select返回的結果集很是大時將會撐爆Client端的內存, JDBC下就是普通的OOM; 固然用MyBatis之類的ORM也有一樣的問題, 由於這些東西都是架構在JDBC之上的.

解決辦法:
1. 使用Client Side Cursor
PreparedStatement/Statement的setFetchSize方法設置爲Integer.MIN_VALUE或者使用方法Statement.enableStreamingResults(), 其實這個方法和設置Integer.MIN_VALUE同樣, 源碼以下:

public void enableStreamingResults() throws SQLException {
    synchronized (checkClosed().getConnectionMutex()) {
        this.originalResultSetType = this.resultSetType;
        this.originalFetchSize = this.fetchSize;

        setFetchSize(Integer.MIN_VALUE);
        setResultSetType(ResultSet.TYPE_FORWARD_ONLY);
    }
}
    
    在網上查了下這種Client Side Cursor的大概實現, 其實mysql自己並無FetchSize方法, 它是經過使用CS阻塞方式的網絡流控制實現服務端不會一下發送大量數據到客戶端撐爆客戶端內存, 我認爲這種方式很是LOW! 是很明顯的"補丁"策略; 這樣就會形成一個必然的問題就是若是沒有所有讀取完ResultSet的結果再執行其餘sql, 那麼將會影響該鏈接的緩存, 因此這種方式要求要麼讀取完ResultSet中的所有數據要麼須要本身調用ResultSet.close()方法, 也就是得用try {} finally{ rs.close(); }或者jdk7下的try-with-resources語法, 例如:
try (ResultSet rs = pstmt.executeQuery()) {
    Role role = new Role();
    int i = 0;
    while (rs.next()) {
        try {
            role.setRoleId(rs.getString("roleId"));
            role.setState(rs.getInt("state"));
            role.setMiscData(rs.getString("miscData"));

            selectHandler.action(role);
        } catch (Exception ex) {
            logger.error("selectAllRoles error!", ex);
        }
    }
}
    
    經常使用的ORM MyBatis下, 默認select的結果是一個List<XXXObject>, 這樣問題就更明顯了, 要將select所有結果放到一個集合中再處理, 那麼結果集一大OOM是必然; 通過查詢MyBatis資料發現有ResultHandler機制, 就是這樣handler:
    sqlSession.select("chenlong.mybatislearn.db.mapper.RoleMapper.findAllRoles", handler);
    可是和JDBC方式同樣, MyBatis即使用了ResultHandler也是將全部結果都讀到Client Side, 內存同樣爆掉, 最後總算髮現xml mapper裏能夠配置select的fetchSize, 按照前面JDBC方式將其配置爲Integer.MIN_VALUE即-2147483648就正常了, 以下:
    <select id="findAllRoles" fetchSize="-2147483648" resultType="chenlong.mybatislearn.db.struct.Role">
        SELECT * FROM role
    </select>
    但還有一個問題就是這種方式必須本身ResultSet.close(), 經過扒MyBatis代碼發現它已經幫咱們作了, 以下sql

  這樣就能夠放心的在MyBatis下使用Client Side Cursor了.緩存


2. 使用Server Side Cursor
    MySQL JDBC Driver文檔中有這樣參數的說明:網絡


useCursorFetch

If connected to MySQL > 5.0.2, and setFetchSize() > 0 on a statement, should that statement use cursor-based fetching to retrieve rows?

Default: false

Since version: 5.0.0 mybatis


    在MyBatis中位置爲:
<property name="url" value="jdbc:mysql://localhost:3008/mybatislearn?autoReconnect=true&amp;useCursorFetch=true"/>

    實測這種Server Side Cursor執行sql後要等好久才開始返回結果, 而Client Side Cursor幾乎是瞬間就開始返回結果; 網上查詢後的結果是Server Side Cursor使用MySQL Server端的資源(內存/CPU……)處理Cursor, 這個多是其緣由, 但一旦開始返回結果目測二者差異不大.
    
    二者各有優缺點, 尤爲是Client Side Cursor必須本身記得ResultSet.close()不然整個鏈接將再也不可用, 此爲大坑, 尤爲是有鏈接池的狀況.架構

  再次也發現MySQL相比其餘大型RDBMS的弱點, 這種查詢遊標遍歷本該是標配! 而MySQL用這麼LOW的實現, 還須要用戶掌握這麼多黑魔法……F***app

 

  參考代碼以下:ide

http://files.cnblogs.com/files/logicbaby/MyBatisLearn.zipfetch

相關文章
相關標籤/搜索