新闻详情

新闻详情

首页 / 资讯中心 / 详情

还在用分页?试试MyBatis流式查询,强的一批!

发布时间:2026/9/28 18:23:31来源:尧图网络
还在用分页?试试MyBatis流式查询,强的一批!
1. 百万行导出把服务打挂的那一夜先说结论MyBatis 流式查询Cursor / ResultHandler能让你在百万行数据导出时把 JVM 内存压成一条平线而传统分页在深分页场景下既慢又容易把数据库连接池拖垮。这篇就围绕 MyBatis 大结果集场景把分页和流式查询的内存、响应差异讲透给出 Cursor 和 ResultHandler 两种写法的可复制配置再演示怎么用 TaoToken 统一 Key/API 通道接入 AI 工具辅助生成 Mapper 骨架目标是跑通百万行导出不 OOM。如果你做过数据迁移、报表导出、批量对账这类需求大概率遇到过这种场面本地跑得好好的一上生产查 100 万行服务直接 OOM或者数据库 CPU 飙到 100%接口超时。很多人第一反应是加分页LIMIT 0,1000、LIMIT 1000,1000一页页捞。分页不是不能用但深分页的LIMIT 1000000,1000会让 MySQL 扫描并丢弃前 100 万行越翻越慢而且每页都要重新建立结果集框架还要把每页结果封装成实体对象时间和内存双重浪费。流式查询换了个思路查询成功后不返回集合而是返回一个迭代器应用每次从迭代器取一条或一批结果。数据库连接保持打开服务端游标逐行吐数据客户端内存只跟批处理大小有关不跟总行数有关。这就是百万行导出不 OOM 的核心。下面从场景对比开始一步步把配置和验证动作落地。2. 分页 vs 流式内存与响应到底差在哪2.1 常规分页查询的写法与代价假设单表 100 万行用 MyBatis-Plus 分页查询大概长这样Mapper public interface BigDataSearchMapper extends BaseMapperBigDataSearchEntity { Select(SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ) PageBigDataSearchEntity pageList(Param(page) PageBigDataSearchEntity page, Param(Constants.WRAPPER) QueryWrapperBigDataSearchEntity queryWrapper); }这段代码本身没问题问题在调用方式。如果你在循环里一页页查每页 1000 条查 1000 次每次都要走一遍 SQL 解析、结果集封装、对象映射。深分页时LIMIT偏移量越来越大数据库要扫描并丢弃前面的行响应时间线性上涨。更糟的是如果某一页不小心设成 10 万条JVM 里瞬间多出 10 万个实体对象GC 压力陡增。我试过在测试环境把单页调到 50 万堆内存 2G 的实例直接 Full GC 频繁接口响应从秒级掉到分钟级。分页适合的是「总量可控、页大小合理、能走覆盖索引」的场景一旦总量上百万且要全量处理分页就不是最优解。2.2 流式查询为什么能稳住内存流式查询的内存曲线是平的。非流式查询内存随记录数近乎直线增长流式查询内存大小取决于批处理大小fetchSize这个值越大内存占用越高所以要根据业务设置合适大小。MyBatis 提供了org.apache.ibatis.cursor.Cursor接口它继承了java.io.Closeable和java.lang.Iterable也就是说 Cursor 可关闭、可遍历还提供三个方法isOpen()取数据前判断 Cursor 是否打开只有打开才能取数据isConsumed()判断查询结果是否全部取完getCurrentIndex()返回已经获取了多少条数据。用流式查询有两个必须记住的点。第一执行流式查询后数据库访问框架不负责关闭连接需要应用在取完数据后自己关闭。第二必须先读取或关闭结果集中的所有行然后才能对连接发出任何其他查询否则会抛异常。因为流式查询会独占连接所以必须尽快处理完。2.3 两种流式写法的定位差异MyBatis 流式查询常见两种落地方式。一种是 Cursor 游标查询Mapper 方法直接返回CursorT调用方用try-with-resources遍历适合自己控制读取节奏、边读边写的场景。另一种是 ResultHandlerMapper 方法返回void通过Options配fetchSize和resultSetType再传一个ResultHandler处理每条结果适合把处理逻辑内聚到回调里。两者底层都依赖服务端游标和fetchSize。区别在于 Cursor 把迭代器交给调用方ResultHandler 把每条结果推给回调。选哪个看你的处理逻辑是「拉」还是「推」。下面两节分别给出可复制配置。3. TaoToken 前置统一 Key 与 API 通道在写 Mapper 骨架之前先花几分钟把 AI 辅助通道准备好。TaoToken 提供统一的 Key 和 API 通道官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。它的作用是让你用一套 Key 就能调用多种模型能力不用在多个平台之间来回切换配置。具体操作路径先到控制台创建 API Key控制台地址 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 在 API Keys 页面生成密钥页面地址 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。拿到 Key 之后如果你只是想先验证模型能不能正常对话可以去模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 试一句。如果你打算长期用 AI 辅助编码、跑 Agent 任务建议看 Coding Plan地址 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按套餐走比单次调用更划算。接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面写了不同语言和工具的配置方式。如果你用的是 Claude Code 这类工具参考 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 的说明配置即可。把 Key 配好之后后面让 AI 生成 Mapper 骨架、补全Options参数、解释报错都能直接走这个通道。注意Key 属于敏感凭证不要硬编码进提交到仓库的配置文件建议用环境变量或配置中心注入。4. 可复制配置Cursor 与 ResultHandler 两种写法4.1 Cursor 游标查询完整配置先看 Cursor 写法。Mapper 接口返回CursorTMapper public interface BigDataSearchMapper extends BaseMapperBigDataSearchEntity { Select(SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ) Options(resultSetType ResultSetType.FORWARD_ONLY, fetchSize 1000) CursorBigDataSearchEntity streamList(Param(Constants.WRAPPER) QueryWrapperBigDataSearchEntity queryWrapper); }resultSetType ResultSetType.FORWARD_ONLY表示结果集游标只能向下滚动这是流式查询的常见配置。fetchSize 1000表示每次从服务端取 1000 条这个值决定内存占用不要盲目设成 100 万。调用方这样写Service public class ExportService { Resource private BigDataSearchMapper bigDataSearchMapper; public void exportAll(QueryWrapperBigDataSearchEntity wrapper) { try (CursorBigDataSearchEntity cursor bigDataSearchMapper.streamList(wrapper)) { cursor.forEach(entity - { // 逐条处理比如写入文件、发送到下游 writeToFile(entity); }); } catch (IOException e) { throw new RuntimeException(导出失败, e); } } }try-with-resources保证 Cursor 用完自动关闭连接随之释放。这里的关键是不要在forEach里再发起其他数据库查询因为流式查询独占连接中途发新查询会抛异常。如果确实要查别的表先把数据读出来暂存或者用独立的连接。4.2 ResultHandler 回调写法完整配置ResultHandler 写法把处理逻辑放进回调Mapper 方法返回voidMapper public interface BigDataSearchMapper extends BaseMapperBigDataSearchEntity { Select(SELECT bds.* FROM big_data_search bds ${ew.customSqlSegment} ) Options(resultSetType ResultSetType.FORWARD_ONLY, fetchSize 1000) ResultType(BigDataSearchEntity.class) void listData(Param(Constants.WRAPPER) QueryWrapperBigDataSearchEntity queryWrapper, ResultHandlerBigDataSearchEntity handler); }注意返回类型必须是void因为查询结果在ResultHandler里处理。调用方用 lambda 实现处理逻辑Service public class BatchProcessService { Resource private BigDataSearchMapper bigDataSearchMapper; public void processAll(QueryWrapperBigDataSearchEntity wrapper) { bigDataSearchMapper.listData(wrapper, resultContext - { BigDataSearchEntity entity resultContext.getResultObject(); // 逐条处理 handle(entity); }); } }ResultType(BigDataSearchEntity.class)告诉 MyBatis 把每行转换成哪个实体。fetchSize同样控制每次获取量。ResultHandler 的好处是处理逻辑内聚不用自己管 Cursor 的关闭框架会处理。但要注意回调里同样不要发起新的数据库查询。4.3 fetchSize 与 resultSetType 参数对照参数可选值作用建议resultSetTypeFORWARD_ONLY游标只能向下滚动流式查询首选resultSetTypeSCROLL_INSENSITIVE游标可上下移动数据库变化时结果集不变一般不用resultSetTypeSCROLL_SENSITIVE游标可上下移动数据库变化时结果集同步改变一般不用fetchSize正整数每次从服务端获取的行数按内存和网络调1000 起步fetchSize不是越大越好。设得太大客户端一次缓存太多行内存又上去了设得太小网络往返次数多整体变慢。1000 到 5000 是比较稳的区间具体看单行数据大小和网络状况。5. 验证请求与成功结果5.1 用 AI 辅助生成 Mapper 骨架配置好 TaoToken 通道后可以让 AI 帮你生成 Mapper 骨架省去手写注解的功夫。比如在模型对话页输入这样的提示帮我写一个 MyBatis Mapper 接口查询 big_data_search 表全量数据 要求使用流式查询返回 CursorBigDataSearchEntity 配置 resultSetType 为 FORWARD_ONLYfetchSize 为 1000 使用 MyBatis-Plus 的 QueryWrapper 作为条件参数。AI 会返回类似第 4.1 节的代码。拿到之后自己核对一遍注解和泛型确认Options参数符合预期。这一步的价值在于减少样板代码的拼写错误尤其是ResultSetType这种容易记混的枚举。生成完骨架再让它补一段调用示例和单元测试思路能省不少时间。5.2 百万行导出的验证动作验证流式查询是否真的不 OOM可以按这个步骤来。第一步准备一张 100 万行的测试表用存储过程或批量插入造数据。第二步把 JVM 堆内存限制到 512M启动参数加-Xmx512m。第三步分别跑分页全量查询和流式查询用jconsole或VisualVM观察堆内存曲线。分页全量查询在堆内存曲线上会看到明显的锯齿每次翻页内存上涨再回落深分页时响应时间越来越长。流式查询的堆内存曲线基本是平的只在fetchSize批次处理时有小幅波动。第四步统计总耗时和 GC 次数。实测下来百万行导出场景流式查询的 Full GC 次数通常是个位数分页则可能几十次甚至上百次。5.3 成功结果的特征跑通之后你会看到这些现象接口在几分钟内处理完 100 万行堆内存没有持续上涨没有 OOM 异常数据库连接在try-with-resources结束后正常归还连接池。日志里能看到getCurrentIndex()递增到 100 万说明数据全部读完。如果中途抛了Streaming result set is still active之类的异常说明你在流式读取过程中发起了新的查询需要调整处理逻辑。6. 本篇常见错排查6.1 流式查询中途发新查询报错最常见的报错是流式结果集还没读完就发起了新查询。原因是流式查询独占数据库连接MyBatis 不允许在同一个连接上同时有两个活跃结果集。解决办法是把新查询的数据提前查出来或者用独立的数据源连接。如果处理逻辑里必须查关联表考虑先批量把关联数据加载到本地缓存再在流式遍历时从缓存取。6.2 fetchSize 设置无效有人发现设了fetchSize但内存还是涨。先确认resultSetType是不是FORWARD_ONLY如果不是MySQL 驱动可能不会启用真正的流式读取。另外MySQL 驱动需要配合useCursorFetchtrue参数才能让fetchSize生效连接串里加上这个参数jdbc:mysql://localhost:3306/test?useCursorFetchtrueuseSSLfalse如果用的是其他数据库参数名可能不同查对应驱动的文档确认。6.3 Cursor 忘记关闭导致连接泄漏Cursor 实现了Closeable但如果你手动new或者没有用try-with-resources忘记关闭就会导致连接泄漏。连接池被占满后后续请求全部阻塞。排查方法是看连接池活跃连接数是否持续上涨不回落。修复方式很简单所有 Cursor 都用try-with-resources包起来或者用 ResultHandler 写法让框架管理。6.4 分页深分页慢的替代方案如果你暂时不想改流式深分页也有优化空间。比如用游标分页基于上次查询的最大 ID 继续查避免LIMIT大偏移量。但游标分页要求有单调递增的排序字段且不能跳页。对于全量导出这种场景流式查询还是更直接。如果只是接口分页展示游标分页够用。6.5 处理逻辑太慢拖垮整体流式查询把数据读出来后如果每条处理逻辑很重比如每条都写一次数据库整体会很慢。这时候要改成批量处理攒够一批再写。注意攒批的容器要及时清空避免临时容器本身变成内存泄漏点。批大小和fetchSize配合调通常批大小等于或略小于fetchSize比较合适。7. 接入文档与工具分流排障和接入相关的问题优先看 API Keys 页面 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 和接入文档 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面覆盖了 Key 管理、参数配置、常见错误码。想先验证模型输出质量去模型对话页 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentchatutm_campaignrewrite 直接试。长期做编码辅助、跑 Agent 任务看 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。Claude Code 用户参考 https://taotoken.net/claudecode-anthropic?utm_sourcetaotoken_aicg_blog_endutm_contentclaudecode-anthropicutm_campaignrewrite 。最后留一个实操建议把fetchSize和批处理大小做成配置项不同环境用不同值。测试环境可以设小一点方便观察生产环境根据单行大小和内存上限调。流式查询不是银弹它适合全量遍历、数据迁移、批量导出这类场景不适合需要随机跳页的交互式分页。选对场景百万行导出不 OOM 就是水到渠成的事。
网站建设高端定制企业官网
RELATED

相关资讯

更多精彩内容,欢迎继续阅读

较早相关资讯

最新相关资讯

Claude Code 上下文太长被截断怎么办?TaoToken 统一 Key 下的 /compact、.claudeignore 与 CLAUDE.md 配置骨架 2026/9/28 19:17:32

Claude Code 上下文太长被截断怎么办?TaoToken 统一 Key 下的 /compact、.claudeignore 与 CLAUDE.md 配置骨架

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

阅读更多 →
嵌入式MCU开发全流程:编译、烧录与仿真实践指南 2026/9/28 19:17:24

嵌入式MCU开发全流程:编译、烧录与仿真实践指南

很多刚接触嵌入式MCU开发的朋友,经常被“编译、烧录、仿真”这三个词绕晕:代码写完了编译一下,报错;好不容易编译通过,烧录时又提示连不上设备;好不容易烧进去,程序却不按预期跑。我在带新人和做…

阅读更多 →
python执行脚本快捷方式 2026/9/28 19:17:11

python执行脚本快捷方式

1.复制代码echo off chcp 936 >nul cd /d "%~dp0" python 截图.py %* echo. echo finished echo (book counts are in the last lines above, or in logs folder) echo pause >nul2.将文件名字改名 截图.py改成你想要运行的项目名称.py3.将文件保存成bat后…

阅读更多 →
超薄电池产品开关机方案选型:DFN负载开关的工程实践 2026/9/28 19:17:11

超薄电池产品开关机方案选型:DFN负载开关的工程实践

做超薄电池产品这两年,我在硬件选型上最有发言权的就是开关机方案。TWS充电仓、智能工牌、电子价签、智能戒指这种贴着电池做的产品,PCB厚度被卡得死死的,以前常用的滑动开关、轻触开关根本塞不进去,而软件软开关那套自锁电路也经…

阅读更多 →
超薄电池产品开关机方案如何选?DFN单键开关机芯片专治量产通病 2026/9/28 19:17:11

超薄电池产品开关机方案如何选?DFN单键开关机芯片专治量产通病

前段时间朋友那边一款超薄磁吸移动电源(总厚度不到9毫米那种)量产遇到了糟心事:几十台机器在老化房里关机再开机,怎么都点不亮。我过去一看,问题不在电池,也不在MCU,而是卡在开关机方案上——他…

阅读更多 →
PS5 PKG游戏安装全攻略:从环境准备到后台安装与排障 2026/9/28 19:17:11

PS5 PKG游戏安装全攻略:从环境准备到后台安装与排障

1. 先搞清楚一件事:PKG安装到底在解决什么问题这两年PS5的玩家社区里,"PKG"这个词出现的频率越来越高,很多刚接触的新手一脸懵:这不是PS3、PS4时代的东西吗?怎么PS5又开始流行了?其实逻辑完全一样…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

联系尧图顾问,获取一对一建站咨询

立即免费咨询 📞 400-888-8888
📞 ✉