Poison

ResultSet

MySQL JDBC 驱动在未配置流式读取或游标相关参数的情况下,默认会使用静态结果集进行数据的接收,其中源码位于 com.mysql.jdbc.MysqlIO#readSingleRowSet

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
private RowData readSingleRowSet(long columnCount, int maxRows, int resultSetConcurrency, boolean isBinaryEncoded, Field[] fields) throws SQLException {
RowData rowData;
ArrayList<ResultSetRow> rows = new ArrayList<ResultSetRow>();

boolean useBufferRowExplicit = useBufferRowExplicit(fields);

// Now read the data
ResultSetRow row = nextRow(fields, (int) columnCount, isBinaryEncoded, resultSetConcurrency, false, useBufferRowExplicit, false, null);

int rowCount = 0;

if (row != null) {
rows.add(row);
rowCount = 1;
}

while (row != null) {
row = nextRow(fields, (int) columnCount, isBinaryEncoded, resultSetConcurrency, false, useBufferRowExplicit, false, null);

if (row != null) {
if ((maxRows == -1) || (rowCount < maxRows)) {
rows.add(row);
rowCount++;
}
}
}

rowData = new RowDataStatic(rows);

return rowData;
}

可以看出,使用 ArrayList 将行记录存储至内存中,然后包装为 RowDataStatic 对象返回给调用方。在大多数情况下,这是最有效的实现方式,但是,在对大批量数据的导出过程中,上面的实现方式要求内存必须足以容纳本次查询的结果集,否则会触发 OOM,如果需要解决该问题,可以优化为使用流式读取,使用如下方式创建 Statement

1
2
3
stmt = conn.createStatement(java.sql.ResultSet.TYPE_FORWARD_ONLY,
java.sql.ResultSet.CONCUR_READ_ONLY);
stmt.setFetchSize(Integer.MIN_VALUE);

经过以上的设置,接收的结果集实例即为 RowDataDynamic 对象,源码可以参考:com.mysql.jdbc.MysqlIO#getResultSet,关键部分代码为:

1
2
3
4
5
6
if (!streamResults) {
rowData = readSingleRowSet(columnCount, maxRows, resultSetConcurrency, isBinaryEncoded, (metadataFromCache == null) ? fields : metadataFromCache);
} else {
rowData = new RowDataDynamic(this, (int) columnCount, (metadataFromCache == null) ? fields : metadataFromCache, isBinaryEncoded);
this.streamingData = rowData;
}

RowDataStaticRowDataDynamic 最显著的差别在 next 方法的实现,其中 com.mysql.jdbc.RowDataStatic#next 的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
private int index;

private List<ResultSetRow> rows;

/**
* Creates a new RowDataStatic object.
*
* @param rows
*/
public RowDataStatic(List<ResultSetRow> rows) {
this.index = -1;
this.rows = rows;
}

public ResultSetRow next() throws SQLException {
this.index++;

if (this.index > this.rows.size()) {
afterLast();
} else if (this.index < this.rows.size()) {
ResultSetRow row = this.rows.get(this.index);

return row.setMetadata(this.metadata);
}

return null;
}

可以看出,调用 RowDataStatic 对象的 next 方法时,是直接读取的之前整个结果集读取到内存中的 List<ResultSetRow> rows 中的数据。

com.mysql.jdbc.RowDataDynamic#next 的实现为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
/**
* Returns the next row.
*
* @return the next row value
* @throws SQLException
* if a database error occurs
*/
public ResultSetRow next() throws SQLException {

nextRecord();

if (this.nextRow == null && !this.streamerClosed && !this.moreResultsExisted) {
this.io.closeStreamer(this);
this.streamerClosed = true;
}

if (this.nextRow != null) {
if (this.index != Integer.MAX_VALUE) {
this.index++;
}
}

return this.nextRow;
}

其中 nextRecord 为从数据库获取下一条记录,此处不再粘贴源码,此种实现方式无需单次获取结果集的全部数据至内存,降低了内存占用。

还有一种方式就是使用游标,具体可以查看下方链接。

References

MySQL :: MySQL Connector/J 5.1 Developer Guide :: 5.4 JDBC API Implementation Notes