恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

MyBatis 流式查询实战:避免数据量过大导致 OOM

  • 首页
  • 资讯中心
  • /
  • MyBatis 流式查询实战:避免数据量过大导致 OOM

相关资讯

实测才敢推!2026年最火AI论文写作工具榜单,AI工具一键写高质论文 2026/9/30 2:50:31
Java面试分布式锁,这5种方案对比 2026/9/30 2:50:31
K8s集群Calico与Flannel网络平滑迁移实操 2026/9/30 2:50:31

最新资讯

无标题项目不是问题:不急着起名也能高效推进
电力市场自调度中的分布鲁棒优化与CVaR风险控制实战解析
基于Java和Vue的区块链供应链溯源与可信交易平台设计
基于Android的运动健身App开发实战:从GPS轨迹到数据存储全解析
Java+Vue+区块链:构建可信供应链溯源平台
OpenHarmony上跑Flutter:油耗追踪器实战开发全记录

今日推荐

模型优化器实战:从FP32到INT8的推理加速与精度平衡
LangGraph+FastAPI构建可审计AI编码助手
基于图像预处理与几何特征的人脸脸型发型搭配系统实现

本周热门

从像素到笔画:srt-whiteboard-animation骨架笔迹追踪实现(Zhang-Suen细化+8邻接追踪)
网站建设的英语怎么说?别只背单词,看完这套安全完整流程才敢上线
新手入门看这篇:建设网站加盟避坑指南与SEO实操

本月精选

自研推理加速器Redwood:两周内实现PyTorch模型高效部署的实战教程
V4L2摄像头采集实战:从camera_client.rar到出图全流程解析
从“谁发明了钢琴键”到知识问答智能体:RAG与记忆工程实践

MyBatis 流式查询实战:避免数据量过大导致 OOM

发布时间:2026/9/30 2:50:31
MyBatis 流式查询实战:避免数据量过大导致 OOM 1. 为什么普通查询会导致 OOM在 MyBatis 中常规查询通常调用selectList、selectMap或自定义 Mapper 方法返回List、Map等集合。这些 API 会由 MyBatis 底层通过DefaultResultSetHandler把数据库返回的ResultSet全部读取到内存中并封装为 Java 对象。当数据量较小例如几千行时这种一次性装载的方式简单直观但当结果集达到几十万、上百万甚至千万行时所有行以及对应的 Java 对象会同时驻留在 JVM 堆内存中很容易触发OutOfMemoryError。例如下面这段最普通的查询方式在百万级数据下几乎是必定内存告急的xmlselect idselectAllUsers resultTypecom.example.User SELECT id, name, email FROM t_user /selectjavaListUser users userMapper.selectAllUsers(); for (User user : users) { // 处理每条用户数据 }问题的本质并不在于 SQL 写错而在于「集合接收」这个动作要求数据库驱动一次性返回全部记录并由 MyBatis 把这些记录全部物化成对象。要解决这个问题核心思路是改为逐条读取、逐条处理、用完即释放也就是流式查询。2. 流式查询的核心概念流式查询并不是让数据库不返回数据而是改变客户端接收数据的方式。普通查询是「批量拉取」数据库把符合条件的行全部发送到客户端驱动和 MyBatis 把它们全部暂存到内存中。流式查询则是「边拉取边处理」应用一次只从数据库游标中取出一行处理完后再取下一行内存中始终只保留少量对象。在 JDBC 层这个能力主要由Statement的fetchSize和ResultSet的游标行为控制。MyBatis 在映射 SQL 时是否支持真正的流式读取取决于执行器类型。MyBatis 提供了三种执行器SIMPLE默认执行器每次执行新建Statement执行完关闭执行查询后通常会把结果集读入内存再返回。REUSE复用Statement适合批量执行但查询行为本质上与 SIMPLE 类似。BATCH主要用于批量更新不适合常规查询。真正能实现游标式读取的是将ResultSet的fetchSize设置为较小值并限制ResultSetType为FORWARD_ONLY让驱动按批次与数据库交互而不是一次性把所有行加载到客户端。3. MyBatis 中实现流式查询的三种方式3.1 使用 Cursor 游标MyBatis 3.2 之后提供了CursorT接口。Mapper 方法的返回值可以声明为CursorT这样 MyBatis 不会把结果全部装进 List而是返回一个可遍历的游标对象。xmlselect idselectAllUsersWithCursor resultTypecom.example.User SELECT id, name, email FROM t_user /selectjavapublic interface UserMapper { CursorUser selectAllUsersWithCursor(); }调用方需要拿到SqlSession并在try-finally中关闭游标javatry (SqlSession sqlSession sqlSessionFactory.openSession()) { UserMapper mapper sqlSession.getMapper(UserMapper.class); try (CursorUser cursor mapper.selectAllUsersWithCursor()) { IteratorUser iterator cursor.iterator(); while (iterator.hasNext()) { User user iterator.next(); // 处理单条数据 } } }注意Cursor依赖SqlSession保持打开状态。如果SqlSession提前关闭游标通常无法继续读取数据。此外Cursor 默认不是线程安全的不应在多个线程中同时遍历。3.2 使用 ResultHandler 处理器Mapper 方法还可以额外接收一个ResultHandler参数。这样 MyBatis 每处理一行就会回调一次handleResult由调用方决定如何处理不再要求返回大集合。xmlselect idselectAllUsers resultTypecom.example.User SELECT id, name, email FROM t_user /selectjavapublic interface UserMapper { void selectAllUsers(ResultHandlerUser handler); }调用时传入匿名处理器javauserMapper.selectAllUsers(resultContext - { User user resultContext.getResultObject(); // 处理单条数据例如写入文件或发送到消息队列 });使用ResultHandler时内存水位主要取决于单条记录的大小和处理逻辑不会随着总行数线性增长。它通常是业务代码中最容易落地的方式。3.3 设置 fetchSize 配合 FORWARD_ONLY 结果集无论使用 Cursor 还是 ResultHandler要让 JDBC 驱动真正采取流式分批次拉取还需要在映射语句中配置fetchSize。不同数据库驱动对fetchSize的支持程度不同。xmlselect idselectAllUsersWithCursor resultTypecom.example.User fetchSize1000 resultSetTypeFORWARD_ONLY SELECT id, name, email FROM t_user /select也可以在全局配置中设置defaultFetchSizexmlsettings setting namedefaultFetchSize value1000/ /settingsresultSetTypeFORWARD_ONLY表示结果集只能向前遍历。对于 MySQL 驱动通常还需要在 JDBC URL 上添加useCursorFetchtrue否则较大的结果集可能仍然被一次性读到内存。对于 PostgreSQL驱动默认会按 fetchSize 分批获取对于 Oracle则需要使用ResultSet.TYPE_FORWARD_ONLY配合合适的 fetchSize。实际项目中应结合具体数据库版本进行测试。3.4 三种方式对比与选择建议维度Cursor 游标ResultHandler 处理器fetchSize FORWARD_ONLY适用场景需要在业务代码中手动控制遍历节奏逐条处理处理逻辑与读取过程高度耦合作为底层参数配合 Cursor 或 ResultHandler 使用内存占用低仅保留游标当前位置的对象低仅保留当前处理的行取决于是否与 Cursor/ResultHandler 配合连接占用整个遍历期间占用连接整个遍历期间占用连接同左取决于调用方式实现复杂度中需要管理 SqlSession 与 Cursor 生命周期低直接传入回调即可低但需根据数据库驱动调整参数是否依赖 SqlSession 保持打开是是是典型数据库支持MySQL 需useCursorFetchtruePostgreSQL 默认支持Oracle 需配合 TYPE_FORWARD_ONLY同左同左选择建议如果只是希望快速替换现有大结果集查询优先使用ResultHandler改动最小、语义最清晰如果需要在遍历过程中灵活控制取数节奏、手动 break 或提前终止使用Cursor更合适fetchSize和FORWARD_ONLY不是独立方案而是前两者的必要配套参数必须根据目标数据库驱动确认是否真正生效。4. 流式查询的注意事项4.1 连接占用时间更长流式查询期间数据库连接一直处于活动状态直到游标关闭或遍历完成。如果单次遍历耗时过长会占用连接池中的连接进而拖垮其他请求。因此流式查询不适合直接在在线请求线程中长时间执行更适合数据导出、批量同步、报表生成等离线任务。4.2 必须关闭游标和 SqlSession无论使用 Cursor 还是 ResultHandler都要确保SqlSession、Cursor被正确关闭。推荐使用try-with-resources否则可能造成连接泄漏反而引发更严重的问题。4.3 事务一致性如果遍历过程中其他事务修改了数据不同数据库在默认隔离级别下的表现可能不同。MySQL 默认的 REPEATABLE READ 会给流式查询建立一致性快照Oracle 则可能读到后续提交的变更。对于导出任务是否需要严格一致性快照应根据业务要求评估。4.4 不要在遍历中执行耗时阻塞操作遍历过程中每取一行就同步调用外部接口会导致锁表时间或者连接占用时间被无限拉长。建议先把数据分批写入本地中间文件、对象存储或本地队列再异步处理降低数据库连接占用和事务持续时间。4.5 与分页查询的取舍流式查询解决的是「一次导出/处理大量数据」的问题分页查询解决的是「按页展示」的问题。如果业务可以自然分页使用LIMIT或键集分页往往更可控如果必须导出全量数据流式查询比深度分页的OFFSET方式更稳定。两者不是对立的而是适用场景不同。5. 实战流式导出百万级数据到 CSV下面给出一个使用 MyBatis 流式查询将海量数据导出到 CSV 文件的完整示例。示例中通过ResultHandler逐条处理并使用BufferedWriter分批写入文件避免在内存中保存完整行集合。javapublic class UserExportService { private final SqlSessionFactory sqlSessionFactory; private static final int FLUSH_SIZE 2000; public UserExportService(SqlSessionFactory sqlSessionFactory) { this.sqlSessionFactory sqlSessionFactory; } public void exportToCsv(String filePath) throws IOException { try (SqlSession sqlSession sqlSessionFactory.openSession(); BufferedWriter writer Files.newBufferedWriter(Paths.get(filePath))) { UserMapper mapper sqlSession.getMapper(UserMapper.class); final int[] count {0}; final StringBuilder buffer new StringBuilder(FLUSH_SIZE * 64); mapper.selectAllUsers(resultContext - { User user resultContext.getResultObject(); buffer.append(user.getId()) .append(,) .append(escape(user.getName())) .append(,) .append(escape(user.getEmail())) .append(\n); count[0]; if (count[0] % FLUSH_SIZE 0) { try { writer.write(buffer.toString()); } catch (IOException e) { throw new UncheckedIOException(e); } buffer.setLength(0); } }); if (buffer.length() 0) { writer.write(buffer.toString()); } writer.flush(); } } private String escape(String value) { if (value null) { return ; } return \ value.replace(\, \\) \; } }耗时较长的导出任务通常运行在后台线程或定时任务中并设置独立的连接池避免与在线业务争抢连接。可以在任务启动前打印数据量估计值和每批次读取数量便于监控进度。6. 常见问题排查6.1 仍然发生 OOM先检查是否真正使用流式接收Mapper 方法如果仍然返回List即使配置了fetchSizeMyBatis 最终可能还是会将结果全部装入 List。还应确认数据库驱动是否支持流式模式例如 MySQL 是否配置了useCursorFetchtrue以及连接 URL 中的其他参数是否与流式模式冲突。6.2 连接被提前关闭常见原因是SqlSession在游标遍历完之前就被关闭。使用ResultHandler的场景中处理逻辑必须位于SqlSession打开期间不要在方法返回后再异步读取游标。使用 Cursor 时也要保证 close 顺序正确。6.3 遍历速度很慢可能是fetchSize设置过小导致来回拉取次数过多。可以适当增大 fetchSize例如从 100 调整到 1000、5000观察吞吐变化。也可能是处理逻辑本身存在大量 IO 或网络等待需要与读取过程解耦。6.4 数据不一致如果在线程遍历期间数据被修改要结合数据库隔离级别和业务要求判断是否可以接受。对导出准确性要求高的场景可以在只读事务中执行或选择业务低峰期执行。7. 最佳实践总结优先使用ResultHandler或Cursor逐条处理避免返回大集合。为批量读取场景配置合理的fetchSize并根据数据库驱动调整连接参数。将流式查询放在后台任务中执行避免长时间占用在线请求线程。使用try-with-resources严格关闭SqlSession与Cursor。部分数据库需要显式开启游标模式如 MySQL 的useCursorFetchtrue。先在小数据集上验证再逐步扩大数据量并监控堆内存、GC 和连接池状态。能分页处理的场景优先分页只能全量导出时再选择流式查询。8. 流式查询在 Spring Boot 中的集成8.1 使用 SqlSessionTemplate 获取 Mapper在 Spring Boot 项目中MyBatis 通常通过 mybatis-spring 与 Spring 事务、连接池集成。流式查询同样可以基于SqlSessionTemplate完成但要特别注意SqlSessionTemplate由 Spring 管理不能像原生 MyBatis 那样手动关闭SqlSession。推荐通过 Mapper 方法直接声明Cursor或ResultHandler参数让 Spring 在事务边界内保持连接打开。javaService public class UserStreamService { private final UserMapper userMapper; public UserStreamService(UserMapper userMapper) { this.userMapper userMapper; } Transactional(readOnly true) public void streamProcess(ConsumerUser consumer) { userMapper.selectAllUsers(resultContext - { User user resultContext.getResultObject(); consumer.accept(user); }); } }这里把处理逻辑放在Transactional(readOnly true)方法内执行目的是让本次流式读取在同一个数据库连接和事务上下文中完成。遍历结束后方法返回连接由 Spring 交还给连接池。8.2 避免在流式遍历时触发独立事务如果在ResultHandler回调里再次调用其他 Mapper 写方法且这些写方法带有独立的Transactional(propagation Propagation.REQUIRES_NEW)会新开一个连接可能与当前流式读取连接互相等待。建议将写入操作批量缓冲到本地队列或内存批次中待读取结束后再统一提交或在同一事务中执行。8.3 为导出任务配置独立线程池耗时导出任务不应占用 HTTP 请求线程。常见做法如下javaAsync(exportExecutor) public void asyncExport(String filePath) throws IOException { userStreamService.streamProcess(user - { // 写入目标文件或消息队列 }); }同时为导出任务配置独立的连接池或连接数上限避免大量流式查询同时执行时把在线业务连接耗尽。9. 性能测试与监控9.1 内存占用对比可以通过简单实验观察普通查询和流式查询的内存差异分别查询 10 万、50 万、100 万行数据使用 JVisualVM 或 Arthas 观察堆内存变化。普通selectList的内存占用会随行数线性上升而ResultHandler模式下堆内存几乎保持平稳。9.2 fetchSize 与吞吐量调优fetchSize过小会导致应用与数据库之间往返次数过多吞吐下降过大则可能让单次批次占用更多网络和内存。建议从 500 到 1000 开始测试再逐步调整为 2000、5000 对比。不同驱动和网络环境下最优值不同应以实测数据为准。9.3 需要监控的关键指标JVM 堆内存和 GC 次数确认是否还会出现内存尖峰。数据库连接池活跃连接数和等待时间避免流式任务占满连接。导出任务耗时、单批写入耗时和最终文件大小。数据库端慢查询和网络传输量。10. 与其他大数据处理方案的对比10.1 分页查询分页查询适合前端按页展示但深分页使用OFFSET会导致数据库扫描成本越来越高。如果只是导出全量数据流式查询通常比深分页更稳定。10.2 数据库自带导出工具MySQL 的mysqldump、SELECT INTO OUTFILEOracle 的 SQL*Plus 等工具也能导出大数据但灵活性不如应用层流式查询。应用层方案可以在读取时同步做字段脱敏、格式转换、业务过滤和上报监控。10.3 批处理框架Spring Batch 等框架提供了 chunk 处理、重试、跳过、作业状态管理等能力适合复杂批处理流程。如果只是简单导出或清洗直接用 MyBatis 流式查询更轻量如果需要调度、断点续跑、失败重试等能力可以结合 Spring Batch 使用。11. 总结MyBatis 流式查询的核心不是使用某个特殊 SQL而是改变数据接收模型从小集合一次性装载改为游标逐条读取。实现上优先使用ResultHandler或Cursor并配合fetchSize和数据库驱动参数让 JDBC 真正进入流式模式。落地时要关注连接占用、事务一致性、关闭顺序和后台任务隔离避免解决了 OOM 又引入连接耗尽或长事务问题。对于必须全量处理的大数据场景MyBatis 流式查询是一个轻量、可控且易于集成的选择。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号