情景: 遍歷並處理一個大表中的所有數據, 這個表中的數據可能會是千萬條或者上億條, 很多人可能會說用分頁limit……但需求本身一次性遍歷更加方便, 且Oracle/DB2都有方便的游標機制.
對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代碼發現它已經幫我們做了, 如下
這樣就可以放心的在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中位置為:
<property name="url" value="jdbc:mysql://localhost:3008/mybatislearn?autoReconnect=true&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***
參考代碼如下:
http://files.cnblogs.com/files/logicbaby/MyBatisLearn.zip