结果集大小控制
查询大量数据时,如果不加控制地将结果全部加载到内存,极易导致 OOM。本文介绍三种核心控制手段。
fetchSize 控制
fetchSize 告知 JDBC 驱动每次从数据库拉取多少行数据,是控制内存消耗的第一道防线。
全局配置
XML
<settings>
<setting name="defaultFetchSize" value="100"/>
</settings>
语句级别配置
XML
<select id="findLargeResult" resultType="User" fetchSize="200">
SELECT * FROM user WHERE status = 1
</select>
fetchSize 工作原理
XML
数据库 ──→ JDBC驱动缓冲 ──→ 应用内存
每次 fetchSize 行 逐批读取
| fetchSize 值 | 行为 |
|---|---|
| 0 或未设置 | 驱动默认行为(通常是全部加载) |
| 正整数 N | 每次从数据库拉取 N 行 |
| -1 | 使用驱动默认值 |
注意:fetchSize 只是给数据库的提示,不同数据库驱动的实现不同,实际效果需验证。
各数据库驱动对 fetchSize 的支持
| 数据库 | fetchSize > 0 效果 |
|---|---|
| MySQL | 需配合 useCursorFetch=true 使用 |
| PostgreSQL | 默认支持,生效良好 |
| Oracle | 默认支持,生效良好 |
MySQL 需在 JDBC URL 中开启游标抓取:
Java
jdbc:mysql://localhost:3306/db?useCursorFetch=true&defaultFetchSize=100
流式查询
流式查询逐行处理结果集,内存中只保留当前行数据,适合超大数据量场景。
MyBatis Cursor 方式
XML
<select id="findAllUsers" resultType="User">
SELECT * FROM user
</select>
Java
// Mapper 接口返回 Cursor
Cursor<User> findAllUsers();
// 使用 Cursor 流式遍历
try (Cursor<User> cursor = mapper.findAllUsers()) {
for (User user : cursor) {
processUser(user);
}
}
注意:Cursor 必须在 SqlSession 未关闭前遍历,且需要保持数据库连接打开。
MyBatis ResultHandler 方式
XML
<select id="processAllUsers" resultType="User">
SELECT * FROM user
</select>
Java
// 使用 ResultHandler 逐行处理
sqlSession.select("com.example.mapper.UserMapper.processAllUsers",
new ResultHandler<User>() {
@Override
public void handleResult(ResultContext<? extends User> context) {
User user = context.getResultObject();
processUser(user);
}
});
ResultHandler 会在每获取一行数据时回调 handleResult 方法。
Cursor 与 ResultHandler 对比
| 特性 | Cursor | ResultHandler |
|---|---|---|
| 遍历方式 | Iterator 遍历 | 回调函数处理 |
| 内存占用 | 仅当前行 | 仅当前行 |
| 使用方式 | 更直观 | 需要实现接口 |
| 适用场景 | 需要控制遍历流程 | 纯逐行处理 |
| 返回值 | Cursor | void |
内存管理最佳实践
1. 分页查询
对于需要展示给用户的列表,必须分页:
XML
<select id="findUsersByPage" resultType="User">
SELECT id, username, email FROM user
ORDER BY id
LIMIT #{offset}, #{pageSize}
</select>
Java
// 合理分页
int pageSize = 20;
int pageNum = 1;
int offset = (pageNum - 1) * pageSize;
List<User> users = mapper.findUsersByPage(offset, pageSize);
2. 只查询必要的列
text
<!-- 反例:加载所有列 -->
<select id="exportEmails" resultType="User">
SELECT * FROM user
</select>
<!-- 正例:只查需要的列 -->
<select id="exportEmails" resultType="String">
SELECT email FROM user WHERE status = 1
</select>
3. 分批处理超大结果集
text
public void processAllUsersInBatches() {
int batchSize = 1000;
int offset = 0;
while (true) {
List<User> batch = mapper.findUsersByPage(offset, batchSize);
if (batch.isEmpty()) {
break;
}
processBatch(batch);
offset += batchSize;
}
}
要点总结
- fetchSize 控制每次从数据库拉取的行数,减少内存峰值
- MySQL 需开启
useCursorFetch=true才能使 fetchSize 生效 - Cursor 和 ResultHandler 实现流式查询,内存中仅保留当前行
- 面向用户的列表必须分页,避免一次性加载全部数据
- 只查询必要的列,减少每行内存占用
- 超大数据量使用分批查询,避免长时间持有数据库连接
📝 发现内容有误?点击此处直接编辑