聊聊jdbc statement的fetchSize

在使用MySQL的JDBC时,如果查询结果集过大,使用一次查询,可能会出现Java.lang.OutOfMemoryError: Java heap space问题,因为DB服务器端一次将查询到的结果集全部发送到Java端保存在内存中而造成OOM。

MySQL JDBC需要一条SQL从数据库读取大量数据,而不发生JVM OOM,可以采用以下方法之一:
    1、当statement设置以下属性时,采用的是流数据接收方式,每次只从服务器接收部份数据,直到所有数据处理完毕,不会发生JVM OOM。

1 setResultSetType(ResultSet.TYPE_FORWARD_ONLY);
2 setFetchSize(Integer.MIN_VALUE); 

    2、调用statement的enableStreamingResults方法,实际上enableStreamingResults方法内部封装的就是第1种方式。

    3、设置连接属性useCursorFetch=true (5.0版驱动开始支持),statement以TYPE_FORWARD_ONLY打开,再设置fetch size参数,表示采用服务器端游标,每次从服务器取fetch_size条数据。

故采用如下方式就可以解决OOM问题:

1 ps = (PreparedStatement) con.prepareStatement("select * from bigTable",
2             ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY);
3 ps.setFetchSize(Integer.MIN_VALUE);
4 ps.setFetchDirection(ResultSet.FETCH_REVERSE);

 

-------------------------------------------------------------------------------------------------

 在Statement和ResultSet接口中都有setFetchSize方法

1 void setFetchSize(int rows) throws SQLException

查看API文档

Statement接口中是这样解释的:

为JDBC 驱动程序提供一个提示,它提示此Statement 生成的ResultSet 对象需要更多行时应该从数据库获取的行数。指定的行数仅影响使用此语句创建的结果集合。如果指定的值为 0,则忽略该提示。默认值为 0。

ResultSet中是这样解释的:

为 JDBC 驱动程序设置此ResultSet 对象需要更多行时应该从数据库获取的行数。如果指定的获取大小为零,则 JDBC 驱动程序忽略该值,随意对获取大小作出它自己的最佳猜测。默认值由创建结果集的Statement 对象设置。获取大小可以在任何时间更改。

网上有下面这样的一段摘录1:

缺省时,驱动程序一次从查询里获取所有的结果。这样可能对于大的数据集来说是不方便的, 因此 JDBC 驱动提供了一个用于设置从一个数据库游标抽取若干行的 ResultSet 的方法。在连接的客户端这边缓冲了一小部分数据行,并且在用尽之后, 则通过重定位游标检索下一个数据行块。

摘录2:

setFetchSize 最主要是为了减少网络交互次数设计的。访问ResultSet时,如果它每次只从服务器上取一行数据,则会产生大量的开销。setFetchSize的意 思是当调用rs.next时,ResultSet会一次性从服务器上取得多少行数据回来,这样在下次rs.next时,它可以直接从内存中获取出数据而不 需要网络交互,提高了效率。 这个设置可能会被某些JDBC驱动忽略的,而且设置过大也会造成内存的上升。

------------------------------------------------------------------------------------------------------------

源码分析:

fetchSize

这里以postgres jdbc driver为例,主要是因为postgres的jdbc driver有公开源码,而且命名比较规范。之前看oracle jdbc,由于没有源码,反编译出来一大堆var1,var2等的变量命名,非常晦涩。

默认情况下pgjdbc driver会一次性拉取所有结果集,也就是在executeQuery的时候。对于大数据量的查询来说,非常容易造成OOM。这种场景就需要设置fetchSize,执行query的时候先返回第一批数据,之后next完一批数据之后再去拉取下一批。

但是这个有几个要求:

  • 数据库必须使用V3协议,即pg7.4+
  • connection的autoCommit必须为false,因为开启autoCommit的话,查询完成cursor会被关闭,那么下次就不能再fetch了。另外ResultSet必须是ResultSet.TYPE_FORWARD_ONLY类型,这个是默认的。也就是说无法向后滚动。
  • 查询语句必须是单条,不能是用分号组成的多条查询

实例代码

 1  @Test
 2     public void testReadTimeout() throws SQLException {
 3         Connection connection = dataSource.getConnection();
 4         //https://jdbc.postgresql.org/documentation/head/query.html
 5         connection.setAutoCommit(false); //NOTE 为了设置fetchSize,必须设置为false
 6 
 7         String sql = "select * from demo_table";
 8         PreparedStatement pstmt;
 9         try {
10             pstmt = (PreparedStatement)connection.prepareStatement(sql);
11             pstmt.setFetchSize(50); 
12             System.out.println("ps.getQueryTimeout():" + pstmt.getQueryTimeout());
13             System.out.println("ps.getFetchSize():" + pstmt.getFetchSize());
14             System.out.println("ps.getFetchDirection():" + pstmt.getFetchDirection());
15             System.out.println("ps.getMaxFieldSize():" + pstmt.getMaxFieldSize());
16 
17             ResultSet rs = pstmt.executeQuery(); 
18             //NOTE 这里返回了就代表statement执行完成,默认返回fetchSize的数据
19             int col = rs.getMetaData().getColumnCount();
20             System.out.println("============================");
21             while (rs.next()) { 
22                 for (int i = 1; i <= col; i++) {
23                     System.out.print(rs.getObject(i));
24                 }
25                 System.out.println("");
26             }
27             System.out.println("============================");
28         } catch (SQLException e) {
29             e.printStackTrace();
30         } finally {
31             //close resources
32         }
33     }

源码解析

postgresql-9.4.1212.jre7-sources.jar!/org/postgresql/jdbc/PgPreparedStatement.java

 1 /*
 2    * A Prepared SQL query is executed and its ResultSet is returned
 3    *
 4    * @return a ResultSet that contains the data produced by the * query - never null
 5    *
 6    * @exception SQLException if a database access error occurs
 7    */
 8   public java.sql.ResultSet executeQuery() throws SQLException {
 9     if (!executeWithFlags(0)) {
10       throw new PSQLException(GT.tr("No results were returned by the query."), PSQLState.NO_DATA);
11     }
12 
13     if (result.getNext() != null) {
14       throw new PSQLException(GT.tr("Multiple ResultSets were returned by the query."),
15           PSQLState.TOO_MANY_RESULTS);
16     }
17 
18     return result.getResultSet();
19   }
20 executeQuery首先调用executeWithFlags方法,源码里头直接写在if里头的,这个不是推荐的方式,因为放在if比较容易忽略。
21 executeWithFlags
22 public boolean executeWithFlags(int flags) throws SQLException {
23     try {
24       checkClosed();
25 
26       if (connection.getPreferQueryMode() == PreferQueryMode.SIMPLE) {
27         flags |= QueryExecutor.QUERY_EXECUTE_AS_SIMPLE;
28       }
29 
30       execute(preparedQuery, preparedParameters, flags);
31 
32       return (result != null && result.getResultSet() != null);
33     } finally {
34       defaultTimeZone = null;
35     }
36   }
37 
38 protected final void execute(CachedQuery cachedQuery, ParameterList queryParameters, int flags)
39       throws SQLException {
40     try {
41       executeInternal(cachedQuery, queryParameters, flags);
42     } catch (SQLException e) {
43       // Don't retry composite queries as it might get partially executed
44       if (cachedQuery.query.getSubqueries() != null
45           || !connection.getQueryExecutor().willHealOnRetry(e)) {
46         throw e;
47       }
48       cachedQuery.query.close();
49       // Execute the query one more time
50       executeInternal(cachedQuery, queryParameters, flags);
51     }
52   }

 

这里又调用execute方法,在调用executeInternal

executeInternal

postgresql-9.4.1212.jre7-sources.jar!/org/postgresql/jdbc/PgPreparedStatement.java

  1 private void executeInternal(CachedQuery cachedQuery, ParameterList queryParameters, int flags)
  2       throws SQLException {
  3     closeForNextExecution();
  4 
  5     // Enable cursor-based resultset if possible.
  6     if (fetchSize > 0 && !wantsScrollableResultSet() && !connection.getAutoCommit()
  7         && !wantsHoldableResultSet()) {
  8       flags |= QueryExecutor.QUERY_FORWARD_CURSOR;
  9     }
 10 
 11     if (wantsGeneratedKeysOnce || wantsGeneratedKeysAlways) {
 12       flags |= QueryExecutor.QUERY_BOTH_ROWS_AND_STATUS;
 13 
 14       // If the no results flag is set (from executeUpdate)
 15       // clear it so we get the generated keys results.
 16       //
 17       if ((flags & QueryExecutor.QUERY_NO_RESULTS) != 0) {
 18         flags &= ~(QueryExecutor.QUERY_NO_RESULTS);
 19       }
 20     }
 21 
 22     if (isOneShotQuery(cachedQuery)) {
 23       flags |= QueryExecutor.QUERY_ONESHOT;
 24     }
 25     // Only use named statements after we hit the threshold. Note that only
 26     // named statements can be transferred in binary format.
 27 
 28     if (connection.getAutoCommit()) {
 29       flags |= QueryExecutor.QUERY_SUPPRESS_BEGIN;
 30     }
 31 
 32     // updateable result sets do not yet support binary updates
 33     if (concurrency != ResultSet.CONCUR_READ_ONLY) {
 34       flags |= QueryExecutor.QUERY_NO_BINARY_TRANSFER;
 35     }
 36 
 37     Query queryToExecute = cachedQuery.query;
 38 
 39     if (queryToExecute.isEmpty()) {
 40       flags |= QueryExecutor.QUERY_SUPPRESS_BEGIN;
 41     }
 42 
 43     if (!queryToExecute.isStatementDescribed() && forceBinaryTransfers
 44         && (flags & QueryExecutor.QUERY_EXECUTE_AS_SIMPLE) == 0) {
 45       // Simple 'Q' execution does not need to know parameter types
 46       // When binaryTransfer is forced, then we need to know resulting parameter and column types,
 47       // thus sending a describe request.
 48       int flags2 = flags | QueryExecutor.QUERY_DESCRIBE_ONLY;
 49       StatementResultHandler handler2 = new StatementResultHandler();
 50       connection.getQueryExecutor().execute(queryToExecute, queryParameters, handler2, 0, 0,
 51           flags2);
 52       ResultWrapper result2 = handler2.getResults();
 53       if (result2 != null) {
 54         result2.getResultSet().close();
 55       }
 56     }
 57 
 58     StatementResultHandler handler = new StatementResultHandler();
 59     result = null;
 60     try {
 61       startTimer();
 62       connection.getQueryExecutor().execute(queryToExecute, queryParameters, handler, maxrows,
 63           fetchSize, flags);
 64     } finally {
 65       killTimerTask();
 66     }
 67     result = firstUnclosedResult = handler.getResults();
 68 
 69     if (wantsGeneratedKeysOnce || wantsGeneratedKeysAlways) {
 70       generatedKeys = result;
 71       result = result.getNext();
 72 
 73       if (wantsGeneratedKeysOnce) {
 74         wantsGeneratedKeysOnce = false;
 75       }
 76     }
 77 
 78   }
 79 主要看这段
 80 connection.getQueryExecutor().execute(queryToExecute, queryParameters, handler, maxrows,
 81           fetchSize, flags);
 82 通过把fetchSize传递进去,拉取指定大小的result
 83 最后调用sendExecute以及processResults方法来拉取数据
 84 postgresql-9.4.1212.jre7-sources.jar!/org/postgresql/core/v3/QueryExecutorImpl.java
 85 
 86 private void sendExecute(SimpleQuery query, Portal portal, int limit) throws IOException {
 87     //
 88     // Send Execute.
 89     //
 90 
 91     if (logger.logDebug()) {
 92       logger.debug(" FE=> Execute(portal=" + portal + ",limit=" + limit + ")");
 93     }
 94 
 95     byte[] encodedPortalName = (portal == null ? null : portal.getEncodedPortalName());
 96     int encodedSize = (encodedPortalName == null ? 0 : encodedPortalName.length);
 97 
 98     // Total size = 4 (size field) + 1 + N (source portal) + 4 (max rows)
 99     pgStream.sendChar('E'); // Execute
100     pgStream.sendInteger4(4 + 1 + encodedSize + 4); // message size
101     if (encodedPortalName != null) {
102       pgStream.send(encodedPortalName); // portal name
103     }
104     pgStream.sendChar(0); // portal name terminator
105     pgStream.sendInteger4(limit); // row limit
106 
107     pendingExecuteQueue.add(new ExecuteRequest(query, portal, false));
108   }
109 
110 protected void processResults(ResultHandler handler, int flags) throws IOException {
111     boolean noResults = (flags & QueryExecutor.QUERY_NO_RESULTS) != 0;
112     boolean bothRowsAndStatus = (flags & QueryExecutor.QUERY_BOTH_ROWS_AND_STATUS) != 0;
113 
114     List<byte[][]> tuples = null;
115 
116     int c;
117     boolean endQuery = false;
118 
119     // At the end of a command execution we have the CommandComplete
120     // message to tell us we're done, but with a describeOnly command
121     // we have no real flag to let us know we're done. We've got to
122     // look for the next RowDescription or NoData message and return
123     // from there.
124     boolean doneAfterRowDescNoData = false;
125 
126     while (!endQuery) {
127       c = pgStream.receiveChar();
128       switch (c) {
129         case 'A': // Asynchronous Notify
130           receiveAsyncNotify();
131           break;
132 
133         case '1': // Parse Complete (response to Parse)
134           pgStream.receiveInteger4(); // len, discarded
135 
136           SimpleQuery parsedQuery = pendingParseQueue.removeFirst();
137           String parsedStatementName = parsedQuery.getStatementName();
138           //...
139       }
140   }
141 }        
142 next
143 postgresql-9.4.1212.jre7-sources.jar!/org/postgresql/jdbc/PgResultSet.java
144 
145 public boolean next() throws SQLException {
146     checkClosed();
147     if (onInsertRow) {
148       throw new PSQLException(GT.tr("Can''t use relative move methods while on the insert row."),
149           PSQLState.INVALID_CURSOR_STATE);
150     }
151     if (current_row + 1 >= rows.size()) {
152       if (cursor == null || (maxRows > 0 && row_offset + rows.size() >= maxRows)) {
153         current_row = rows.size();
154         this_row = null;
155         rowBuffer = null;
156         return false; // End of the resultset.
157       }
158 
159       // Ask for some more data.
160       row_offset += rows.size(); // We are discarding some data.
161 
162       int fetchRows = fetchSize;
163       if (maxRows != 0) {
164         if (fetchRows == 0 || row_offset + fetchRows > maxRows) {
165           // Fetch would exceed maxRows, limit it.
166           fetchRows = maxRows - row_offset;
167         }
168       }
169       // Execute the fetch and update this resultset.
170       connection.getQueryExecutor().fetch(cursor, new CursorResultHandler(), fetchRows);
171       current_row = 0;
172 
173       // Test the new rows array.
174       if (rows.isEmpty()) {
175         this_row = null;
176         rowBuffer = null;
177         return false;
178       }
179     } else {
180       current_row++;
181     }
182     initRowBuffer();
183     return true;
184   }

 

next方法可以看到,首先判断current_row + 1是否小于rows.size(),小于的话,那就current_row++;否则表示这一批fetchSize的数据被消费完了,需要判断是否结束或者拉取下一批数据,之后更新current_row
1 connection.getQueryExecutor().fetch(cursor, new CursorResultHandler(), fetchRows);
这个方法拉取fetchRows条数的下一批数据
  • initRowBuffer
 1 private void initRowBuffer() {
 2     this_row = rows.get(current_row);
 3     // We only need a copy of the current row if we're going to
 4     // modify it via an updatable resultset.
 5     if (resultsetconcurrency == ResultSet.CONCUR_UPDATABLE) {
 6       rowBuffer = new byte[this_row.length][];
 7       System.arraycopy(this_row, 0, rowBuffer, 0, this_row.length);
 8     } else {
 9       rowBuffer = null;
10     }
11   }

 

这就是next移动之后,把要消费的这行数据放到rowBuffer里头。

小结

对于查询数据量大的场景下,非常有必要设置fetchSize,否则全量拉取很容易OOM,但是使用fetchSize的时候,要求数据能够在遍历resultSet的时候及时处理,而不是收集完所有数据返回回去再去处理。

posted @ 2019-01-09 17:57  Boblim  阅读(12182)  评论(0编辑  收藏  举报