一次查 50 万行把老年代打爆:MyBatis 的 fetchSize 不生效,和 Executor 到 ResultSetHandler 的 5 段链路
title: 一次查 50 万行把老年代打爆MyBatis 的 fetchSize 不生效和 Executor 到 ResultSetHandler 的 5 段链路tags: [MyBatis, 源码分析, 性能优化, Java, 后端]category: 后端一次数据导出把生产 JVM 干到 Full GC我们有一个「对账文件导出」的定时任务每次把一张 50 万行的明细表全量捞出来按行写 CSV。代码一直跑得好好的直到有一次表数据涨到 80 万行导出任务所在的应用老年代直接打满连续 Full GC接口大面积超时。最初的写法长这样Select(SELECT id, order_no, amount, create_time FROM settle_detail WHERE biz_date #{date}) ListSettleDetail listByDate(Param(date) String date); // 调用方 ListSettleDetail rows mapper.listByDate(2026-08-01); for (SettleDetail r : rows) { writeCsv(r); // 一次性把 80 万行都装进 List再遍历 }逐行拆解这段第 1 行Select声明了一条全量查询没有分页意图是把当天明细一次性取回。第 2 行返回ListSettleDetail意味着 MyBatis 会先把所有结果行都映射成对象再整体返回给调用方。第 6 行mapper.listByDate返回时这 80 万行对象已经全部驻留在 JVM 堆里。第 7 行才遍历写文件——但此时内存峰值早已在「返回瞬间」到达GC 根本来不及在遍历阶段救场。当时组里第一反应是「加机器、扩内存」但我们没那么大的堆预算于是把 MyBatis 的查询链路从头读了一遍发现真正的问题有两个fetchSize 在我们的 JDBC 配置下根本没生效以及MyBatis 默认把结果集一次性物化到 List。第一段链路SqlSession 到 ExecutorDefaultSqlSession.selectList是最常走的入口它把活儿甩给Executor// DefaultSqlSession.selectList public E ListE selectList(String statement, Object parameter, RowBounds rowBounds) { MappedStatement ms configuration.getMappedStatement(statement); return executor.query(ms, wrapCollection(parameter), rowBounds, Executor.NO_RESULT_HANDLER); }逐行看第 3 行从全局Configuration里按 statement id 取出MappedStatement这里面装着 SQL、参数映射、结果映射等全部元数据。第 4 行交给executor.query。默认是CachingExecutor包了一层SimpleExecutor或ReuseExecutor前者负责二级缓存后者负责真正的 JDBC 交互。CachingExecutor先查二级缓存没命中再委托给被包装的BaseExecutor// BaseExecutor.query public E ListE query(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler) { BoundSql boundSql ms.getBoundSql(parameter); CacheKey key createCacheKey(ms, parameter, rowBounds, boundSql); return query(ms, parameter, rowBounds, resultHandler, key, boundSql); // 走一级缓存 真正查询 }这里要特别注意BaseExecutor维护着一级缓存localCachePerpetualCache它是SqlSession级别的。同一个 SqlSession 内两次相同查询第二次会直接返回一级缓存里的对象——这个机制本身也可能让你在测试里「改了库却查不到新值」不过那是另一个坑本文聚焦执行链路。第二段链路StatementHandler 与 fetchSize真正和 JDBC 打交道的是PreparedStatementHandlerSimpleExecutor.doQuery长这样// SimpleExecutor.doQuery public E ListE doQuery(MappedStatement ms, Object parameter, RowBounds rowBounds, ResultHandler resultHandler, BoundSql boundSql) { Statement stmt null; try { Configuration configuration ms.getConfiguration(); StatementHandler handler configuration.newStatementHandler(wrapper, ms, parameter, rowBounds, resultHandler, boundSql); stmt prepareStatement(handler, ms.getStatementLog()); // 建连接、设参数 return handler.query(stmt, resultHandler); // 执行并映射 } finally { closeStatement(stmt); } }关键点藏在prepareStatement里它最终调用 JDBC 的PreparedStatement.setFetchSize// PreparedStatementHandler 在 instantiateStatement 之后 stmt.setFetchSize(fetchSize); // fetchSize 来自 MappedStatement默认是 null逐行拆解fetchSize控制 JDBC 驱动每次从数据库网络缓冲取多少行到客户端。设成 100就分批拉内存平稳不设置MySQL 驱动默认会一次性把结果集全部取回。MyBatis 的fetchSize默认值在MappedStatement里是null最终落到 JDBC 时如果不显式配就用驱动默认——而 MySQL Connector/J 在没开游标时默认等于把整个结果集拉进内存。我们当时的mybatis-config.xml里压根没配defaultFetchSize所以 80 万行被一次性物化进堆。补上配置只是第一步因为 MySQL 还需要在连接串里开useCursorFetchtrue才能让fetchSize真正走服务端游标否则驱动会忽略它。第三段链路ResultSetHandler 把行映射成对象PreparedStatementHandler.query把结果交给DefaultResultSetHandler// DefaultResultSetHandler.handleResultSets public ListObject handleResultSets(Statement stmt) throws SQLException { final ListObject multipleResults new ArrayList(); int resultSetCount 0; ResultSetWrapper rsw getFirstResultSet(stmt); while (rsw ! null) { ResultMap resultMap resultMaps.get(resultSetCount); // 把当前 ResultSet 逐行映射全部 add 进 localResults handleResultSet(rsw, resultMap, multipleResults, null); rsw getNextResultSet(stmt); resultSetCount; } return collapseSingleResultList(multipleResults); // 最终返回一个 List }逐行看第 4 行getFirstResultSet拿到 JDBC 的ResultSet。第 7 行handleResultSet内部会循环rs.next()每一行都createResultObject生成一个 Java 对象并塞进一个List。第 10 行collapseSingleResultList把结果整体返回——这就是「一次性物化」的根源MyBatis 默认把 ResultSet 全部转成对象再给你。也就是说即便你在 JDBC 层开了fetchSize做流式拉取只要调用的是selectListMyBatis 仍然会在内部把所有行映射完才返回。80 万行对象在返回前就已经堆满了老年代。真正的解法用 ResultHandler 流式消费而不是 selectList要切断「全量物化」得绕开selectList改用ResultHandler逐行处理// 用 SqlSession 的 select 重载配合 ResultHandler 逐行处理 try (SqlSession session sqlSessionFactory.openSession(ExecutorType.SIMPLE, false)) { OrderMapper mapper session.getMapper(OrderMapper.class); mapper.listByDate(2026-08-01, resultContext - { SettleDetail row resultContext.getResultObject(); writeCsv(row); // 取一行、写一行、丢一行 }); }// Mapper 接口改成接收 ResultHandler Select(SELECT id, order_no, amount, create_time FROM settle_detail WHERE biz_date #{date}) void listByDate(Param(date) String date, ResultHandlerSettleDetail handler);逐行拆解这套写法第 3 行ResultHandler的回调在DefaultResultSetHandler每映射完一行时就被调用一次对象出来立刻进writeCsv。第 5 行writeCsv(row)处理完就失去强引用下一行映射时被 GC 回收内存峰值从「80 万行」降到「1 行」。第 10 行 Mapper 方法返回类型改成void并接收ResultHandlerMyBatis 就不会再帮你攒 List。配套的 JDBC 层还要在连接串加useCursorFetchtrue并设defaultFetchSize500让驱动也分批拉双管齐下。方案对比四种导出姿势方案内存峰值改动量风险selectList 全量返回80 万行对象全在堆最小大数据量直接 OOM/Full GCselectList 数据库分页受每页大小控制中分页查询次数多、深翻页慢游标 ResultHandler约 1 行中需改 Mapper 签名、注意事务边界直接 JDBC ResultSet 流式约 1 行大脱离 MyBatis 映射要手写我们最后选了「游标 ResultHandler」因为它既保留了 MyBatis 的结果映射又把内存压到最低。分页方案在我们这种「必须全量、顺序写文件」的场景下反而更慢。复盘数字改造前导出 80 万行老年代峰值约 1.6 GB期间触发 3 次 Full GC单次停顿 1.2~1.8 秒应用 P99 从 120ms 飙到 2.3s。改造后同样 80 万行老年代峰值稳定在 120 MB 以内全程零 Full GC导出耗时反而从 47 秒降到 31 秒少了 GC 停顿。全代码库里类似「一次性 selectList 大结果集」的写法静态扫描又找出 5 处全部在后续迭代里改成流式。我的取舍我觉得selectList是个「新手友好但暗藏上限」的 API。小数据量几千行以内用它最省事没有任何问题可一旦结果集可能涨到十万、百万级它就从「方便」变成「隐患」。我更倾向于在代码评审里给「查询返回 List」加一个隐形的规模红线超过一万行的结果强制走ResultHandler或数据库游标。别等 JVM 用 Full GC 提醒你——那个提醒的代价往往是一次线上抖动。思考题你项目里有没有「一次查全表再内存处理」的 Mapper把它的返回类型从List改成voidResultHandler跑一遍大数据量看看老年代曲线会不会从尖峰变成一条平线。

相关新闻

免费开源的WPS AI插件 察元AI助手:从 Ribbon 加载到 AI 对话框 URL 拼装

免费开源的WPS AI插件 察元AI助手:从 Ribbon 加载到 AI 对话框 URL 拼装

摘要OnAddinLoad 在加载项生命周期早期注册 ApiEvent、恢复模型索引并异步 loadModelList。openAIAssistantDialog 使用 Util.GetUrlPath 与 GetRouterHash 拼接 SPA 地址,是与 WPS ShowDialog 强耦合的关键路径。关键词OnAddinLoad;ShowDialog;ribbon扩展阅读与维护…

2026/10/19 18:54:22 阅读更多 →
uni-app跨端开发入门:从HBuilderX安装到多端发布实战指南

uni-app跨端开发入门:从HBuilderX安装到多端发布实战指南

1. 从零开始:为什么选择 uni-app 作为你的跨端起点 如果你正在寻找一个能让你“一次开发,多端发布”的解决方案,那么 uni-app 大概率已经出现在你的雷达上了。它不是一个新概念,但确实是在当前多端生态下,对中小团队和…

2026/10/18 23:05:51 阅读更多 →
Linux文件与fd

Linux文件与fd

一个程序#include <stdio.h> #include <string.h>int main() {FILE *fd fopen("hello.txt","w");if(fd NULL){return 1;}const char *msg "hello world\n";fwrite(msg, 1, strlen(msg)1, fd);fclose(fd);return 0; }运行程序之后…

2026/9/27 12:41:02 阅读更多 →

最新新闻

Java 基础核心知识学习记录

Java 基础核心知识学习记录

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 18:47:41 阅读更多 →
3分钟理清AQS与其子类的那些事

3分钟理清AQS与其子类的那些事

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 18:41:37 阅读更多 →
【拯救HMI】:工业4.0时代,为什么你的设备还长着一张“2.0时代的脸”?

【拯救HMI】:工业4.0时代,为什么你的设备还长着一张“2.0时代的脸”?

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 18:40:37 阅读更多 →
Java程序员转AI开发:不用先清空重学,重点补这4项能力

Java程序员转AI开发:不用先清空重学,重点补这4项能力

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 18:40:37 阅读更多 →
《软件工程原理与实践》Lab3——卷积神经网络

《软件工程原理与实践》Lab3——卷积神经网络

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 18:39:36 阅读更多 →
m3u8视频下载全攻略:从HLS协议原理到在线工具实操与避坑指南

m3u8视频下载全攻略:从HLS协议原理到在线工具实操与避坑指南

简介&#xff1a;这是一款面向流媒体视频下载需求的 m3u8 在线下载与提取工具&#xff0c;适合需要将 HLS 流媒体保存为本地视频的普通用户、视频运营人员及前端开发者。工具围绕 M3U8 清单解析、TS 分片多线程下载和自动合并设计&#xff0c;同时提供网页端交互界面&#xff0…

2026/10/19 18:38:35 阅读更多 →

日新闻

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

频率跟踪型PWM电源设计:臭氧发生器谐振点漂移与软开关实现

简介&#xff1a;本资源为电源技术方向的学术研究文档&#xff0c;聚焦介质阻挡放电型臭氧发生器电源的频率跟踪型PWM控制策略&#xff0c;面向电力电子、高压电源及臭氧应用领域的科研人员与工程技术人员。针对负载呈容性、等效电容随外加电压升高而增大、谐振频率随之漂移的难…

2026/10/19 0:00:47 阅读更多 →
Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

Vibe Coding 遇上 TaoToken:把开发者思维圣殿的灵感流接入统一 Key 通道

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 0:00:47 阅读更多 →
HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

HundredCV-Chat话题建模实战教程:利用4,871个内置话题标签构建主题分析系统

【免费下载链接】HundredCV-Chat 项目地址&#xff1a; https://ai.gitcode.com/hf_mirrors/Jax-dan/HundredCV-Chat 点击查看 免费下载 HundredCV-Chat 是一个由 100 位青年简历数据开发而来的中文多轮对话数据集&#xff0c;共包含 24,750 组日常闲聊对话和 4,871 个话题标签…

2026/10/19 0:00:47 阅读更多 →

周新闻

基于GMM的语音识别系统原理与MATLAB实现详解

基于GMM的语音识别系统原理与MATLAB实现详解

说到基于高斯混合模型&#xff08;GMM&#xff09;的语音识别系统&#xff0c;很多人第一反应就是一堆数学公式&#xff0c;觉得门槛很高。但如果你真的在MATLAB里把它落地跑通&#xff0c;会发现核心其实就是三件事&#xff1a;把声音变成特征向量&#xff0c;用GMM给每个类别…

2026/10/19 10:02:04 阅读更多 →
数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理入门:反白、灰度化、调色板修改与导出txt实践

数字图像处理这四个字听起来挺唬人&#xff0c;但落到具体操作上&#xff0c;无非就是让计算机按你的想法“摆弄”像素。我在一个模拟项目X里用了一套特别经典的基础处理流程&#xff1a;对图像做反白、改调色板、彩色转灰度&#xff0c;最后把像素数据导出成txt文件。单看每一…

2026/10/19 17:32:41 阅读更多 →
机器人大脑技术盘点:具身智能的感知、决策与数据闭环

机器人大脑技术盘点:具身智能的感知、决策与数据闭环

如果你的信息流最近也天天刷到“机器人大脑”这个词&#xff0c;你会发现一个很有意思的现象&#xff1a;每个人说的“大脑”根本不是同一个东西。有人指的是大语言模型&#xff0c;有人指的是运动控制系统&#xff0c;还有人指的是能理解物理世界的端到端模型。在2026年这个时…

2026/10/19 1:30:52 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 2:54:50 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/19 9:10:22 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/18 12:16:16 阅读更多 →