新闻详情

新闻详情

首页 / 资讯中心 / 详情

MySQL 百万数据导出实战:普通查询、游标查询、流式查询、分页查询与 MyBatis 游标查询配 TaoToken

发布时间:2026/9/26 12:11:40来源:尧图网络
MySQL 百万数据导出实战:普通查询、游标查询、流式查询、分页查询与 MyBatis 游标查询配 TaoToken
1. 百万数据导出为什么会 OOM先把场景说清楚MySQL 百万数据导出听起来只是「查出来写 Excel」真跑起来却经常在半夜把服务打挂。核心检索词就几个mysql、游标查询、流式查询、分页查询、mybatis 游标查询。它们解决的是同一件事——别把一百万行一次性塞进 JVM 堆内存。我先把场景摆出来。假设你有一张user表数据量 120 万行字段包括 id、username、age、password、phone、address、status、create_time、update_time。业务方要一个 Excel运营要能直接打开筛选。你写了个最朴素的select * from user然后ListUser list userMapper.selectList(null)本地测试 2 万行没问题上线跑 120 万行堆内存直接飙到 4GGC 疯狂 Full GC最后java.lang.OutOfMemoryError: Java heap space。问题出在三个地方。第一MySQL JDBC 驱动默认会把整个 ResultSet 拉到客户端内存这叫「全量结果集」。第二MyBatis 的selectList会把所有行映射成对象放进List又是一份内存。第三EasyExcel 如果不用ExcelWriter分批写而是EasyExcel.write(...).sheet().doWrite(list)它内部还会再缓存一份。三份叠加百万行必炸。所以导出的本质是流式管道数据库一行一行吐程序一批一批写内存里始终只保留一个批次。下面我把普通查询、游标查询、流式查询、分页查询、MyBatis 游标查询五种方式拆开讲每种都给可复制的配置和参数最后用 TaoToken 统一 Key 通道接入 AI 工具做验证动作。注意本文所有连接串、账号密码都是本地示例生产环境请用配置中心或环境变量注入别硬编码。2. TaoToken 前置统一 Key 与 API 通道怎么准备在写导出代码之前先把 AI 辅助工具这条链路打通。原因很实际百万数据导出的排障过程里你会反复让 AI 帮你读堆栈、改 SQL、生成 MyBatis 映射如果每个工具都要单独配 Key切换成本很高。TaoToken 的作用就是提供一个统一的 Key 和 API 通道让模型对话、编码计划、控制台管理走同一套凭证。你需要准备的东西一个 TaoToken 账号登录官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册。在控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 里创建 API Key。拿到 Key 后模型对话入口在 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 编码计划在 https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API 基础地址是 https://taotoken.net/api 注意这个地址不带 UTM 参数直接用于代码里的 base_url。配置方式和你平时用 OpenAI 兼容接口一样把 base_url 指向它api_key 填你创建的 Key 即可。这一步不是注册教程注水而是后面验证动作的前提。因为导出代码改完你需要一个稳定的模型通道来跑「让 AI 检查这段 MyBatis 游标配置有没有漏事务」这类动作。Key 拿到手我们进入正题。3. 五种查询方式的可复制配置与参数骨架3.1 普通查询为什么它最先出局普通查询就是select * from user配Statement.executeQuery然后 while 循环读。看起来已经在「一行一行读」了但 MySQL Connector/J 默认行为是把结果集全部加载到客户端。除非你显式设置fetchSize否则驱动会一次性拉完。Connection connection JDBCUtils.getConnection(); Statement statement connection.createStatement(); ResultSet rs statement.executeQuery(select * from user); ListUserExcelVO dataList new ArrayList(); while (rs.next()) { // 映射字段 dataList.add(excelVO); if (dataList.size() 15000) { excelWriter.write(dataList, writeSheet); dataList.clear(); } }这段代码在 2 万行以内没问题因为dataList每 15000 行清一次内存可控。但ResultSet本身在驱动层是全量的120 万行时驱动内部缓冲区就把堆吃满了。所以普通查询适合小数据量百万级直接排除。3.2 游标查询setFetchSize 的正确用法游标查询的关键是PreparedStatement加ResultSet.TYPE_FORWARD_ONLY、ResultSet.CONCUR_READ_ONLY再设置setFetchSize(2000)。这样驱动会按 2000 行一批从服务端拉取而不是一次拉完。PreparedStatement statement connection.prepareStatement( select * from user, ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY ); statement.setFetchSize(2000); ResultSet rs statement.executeQuery(); ListUserExcelVO dataList new ArrayList(); while (rs.next()) { // 映射 dataList.add(excelVO); if (dataList.size() 5000) { excelWriter.write(dataList, writeSheet); dataList.clear(); } }注意fetchSize和dataList批次大小是两个概念。fetchSize控制驱动从 MySQL 拉多少行到客户端缓冲区dataList控制你写 Excel 的批次。两者都设小一点内存曲线会很平。实测 120 万行fetchSize2000、批次 5000堆内存稳定在 300M 以内。3.3 流式查询Integer.MIN_VALUE 的坑流式查询和游标查询很像区别在setFetchSize(Integer.MIN_VALUE)。这是 MySQL Connector/J 的一个特殊约定表示「逐行流式读取」驱动不会在客户端缓存结果集。Statement statement connection.createStatement( ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY ); statement.setFetchSize(Integer.MIN_VALUE); ResultSet rs statement.executeQuery(select * from user);这里有个坑Integer.MIN_VALUE只对Statement生效如果你用PreparedStatement并传了Integer.MIN_VALUE某些驱动版本会抛异常或行为不一致。所以流式查询建议用Statement游标查询用PreparedStatement配正数fetchSize。另外流式读取期间不能在同一连接上执行其他查询否则会报「Streaming result set is still active」。3.4 分页查询limit offset 的深分页问题分页查询是最容易想到的方案PageUser page new Page(current, size)每页 20000 行循环查。MyBatis-Plus 的写法ListUserExcelVO dataList new ArrayList(20000); int current 1, size 20000; PageUser page new Page(current, size); page.setSearchCount(false); do { dataList.clear(); page.setCurrent(current); IPageUser iPage userService.page(page, null); if (CollectionUtils.isEmpty(iPage.getRecords())) { break; } dataList iPage.getRecords().stream().map(user - { UserExcelVO excelVO new UserExcelVO(); BeanUtils.copyProperties(user, excelVO); return excelVO; }).collect(Collectors.toList()); excelWriter.write(dataList, writeSheet); current; } while (dataList.size() size);setSearchCount(false)很重要否则每页都会跑一次count(*)120 万行分 60 页就是 60 次 count白白浪费。但分页查询有个致命问题limit 1000000, 20000这种深分页MySQL 要先扫描前 100 万行再丢弃越翻越慢。所以分页适合数据量几十万以内或者用游标式分页记住上一页最大 idwhere id lastId limit 20000。3.5 MyBatis 游标查询Cursor 配事务MyBatis 的CursorT是对 JDBC 游标的封装写法最优雅但有两个硬性条件连接串加useCursorFetchtrue方法加Transactional。连接串jdbc:mysql://192.168.159.100:3306/ssm?useUnicodetruecharacterEncodingutf-8useSSLtrueserverTimezoneAsia/ShanghaiuseCursorFetchtrueMapper XMLselect idfindUsers resultTypecom.linging.easyexcel.pojo.User fetchSize2000 select * from user /selectService 层Transactional Override public ListUser listMybatisCursorUser() { ListUser list new ArrayList(); try (CursorUser cursor userMapper.findUsers()) { for (User user : cursor) { System.out.println(user); } } catch (IOException e) { throw new RuntimeException(e); } return list; }Controller 里导出Transactional GetMapping(/exportMpCursor) public void exportMpCursor(HttpServletResponse response) { ExcelWriter excelWriter EasyExcel.write(response.getOutputStream(), UserExcelVO.class) .autoCloseStream(true).build(); WriteSheet writeSheet EasyExcel.writerSheet(Sheet1).build(); ListUserExcelVO dataList new ArrayList(); try (CursorUser cursor userMapper.findUsers()) { for (User user : cursor) { UserExcelVO excelVO new UserExcelVO(); BeanUtils.copyProperties(user, excelVO); dataList.add(excelVO); if (dataList.size() 5000) { excelWriter.write(dataList, writeSheet); dataList.clear(); } } } catch (Exception e) { throw new RuntimeException(e); } finally { if (excelWriter ! null) { excelWriter.finish(); } } }Transactional不能省因为 MyBatis 游标依赖连接保持打开状态事务提交前连接不能归还连接池。fetchSize2000写在 XML 里配合连接串的useCursorFetchtrue才生效。实测 120 万行MyBatis 游标方式堆内存稳定在 250M 左右比手写 JDBC 游标还省一点因为 MyBatis 的映射复用做得更好。3.6 五种方式对照方式关键参数内存表现适用量级主要坑普通查询无全量加载易 OOM万级驱动默认全量游标查询fetchSize2000稳定百万级需 PreparedStatement流式查询fetchSizeInteger.MIN_VALUE最省百万级连接独占分页查询setSearchCount(false)稳定几十万深分页慢MyBatis 游标useCursorFetchtrue Transactional稳定百万级必须加事务4. 验证请求与成功结果跑一遍看内存曲线配置写完怎么验证真的不 OOM我一般分三步。第一步本地起服务用jconsole或jvisualvm挂上去看堆内存曲线。跑/user/exportMpCursor观察堆内存是否在 300M 以内波动而不是一路爬升到 4G。如果曲线平稳说明流式管道生效了。第二步看日志里的耗时。每种方式在finally里都打了耗时比如「单线程 mybatis 游标查询导出xxxxms」。120 万行、9 个字段MyBatis 游标方式大概在 40 到 70 秒之间取决于磁盘和网络。如果超过 3 分钟检查fetchSize是不是设太小导致往返次数过多。第三步用 TaoToken 的模型对话通道做一次代码审查验证。把exportMpCursor方法贴进 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 让它检查「事务注解是否遗漏、Cursor 是否在 try-with-resources 里关闭、fetchSize 是否与连接串匹配」。这一步能帮你抓出肉眼容易漏的配置问题。如果你在长期做编码和 Agent 任务可以用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 把这类审查动作固化下来。成功的结果长这样Excel 文件生成完整120 万行数据无缺失服务堆内存峰值不超过 400MGC 次数正常没有OutOfMemoryError也没有Streaming result set is still active这类报错。5. 本篇常见错排查5.1 useCursorFetchtrue 没加Cursor 退化成全量现象MyBatis 游标查询跑起来还是 OOM。原因连接串漏了useCursorFetchtrue驱动不认fetchSizeCursor内部还是全量结果集。排查打印连接串确认参数存在或者用SHOW VARIABLES LIKE have_query_cache之类的方式确认连接属性。修复在 JDBC URL 里补上useCursorFetchtrue。5.2 Transactional 漏加报连接已关闭现象java.sql.SQLException: Operation not allowed after ResultSet closed或Connection is closed。原因MyBatis 游标遍历过程中连接被归还连接池。排查看 Service 或 Controller 方法上有没有Transactional。修复加上Transactional并确保遍历在事务内完成。5.3 fetchSize 设成 Integer.MIN_VALUE 却用了 PreparedStatement现象抛异常或行为异常。原因Integer.MIN_VALUE是Statement的流式约定PreparedStatement应使用正数fetchSize配useCursorFetchtrue。排查看代码里是Statement还是PreparedStatement。修复流式用Statement游标用PreparedStatement。5.4 分页查询深分页越来越慢现象前几页很快翻到后面每页要几十秒。原因limit offset, size的 offset 越大MySQL 扫描丢弃的行越多。排查看慢查询日志里rows_examined是否远大于rows_sent。修复改用游标式分页where id lastMaxId order by id limit 20000或者直接用 MyBatis 游标查询。5.5 ExcelWriter 没 finish文件损坏现象下载的 Excel 打不开或数据不全。原因excelWriter.finish()没调用或者异常路径下没走到。排查看finally块里有没有finish()。修复把finish()放在finally里确保任何路径都执行。5.6 流式读取期间执行其他查询现象Streaming result set is still active。原因同一个连接上流式 ResultSet 没读完就执行了别的 SQL。排查看代码里是否在 while 循环内调用了其他 mapper 方法。修复流式读取期间不要复用同一连接做其他查询或者把其他查询放到独立连接。6. 接入文档与 Key 管理把验证动作固化排障和接入相关的动作统一走 API Keys 和接入文档这条线。Key 管理在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。API 基础地址 https://taotoken.net/api 直接填到你的 HTTP 客户端 base_url 里。如果你要验证模型输出用模型对话 https://taotoken.net/model-chat?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。如果你在做长期编码或 Agent 任务用 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。控制台 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 用来看用量和 Key 状态。最后给一个我踩过的坑MyBatis 游标查询的fetchSize不要设太大2000 到 5000 之间比较稳。设成 50000 虽然往返次数少但驱动缓冲区又会变大内存曲线会抬头。导出百万数据这件事核心就一句话——让数据流起来别让它堆起来。
网站建设高端定制企业官网
RELATED

相关资讯

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

较早相关资讯

最新相关资讯

绿联CH397A USB网卡在Win11/Win10驱动安装与故障排查全攻略 2026/9/27 4:01:11

绿联CH397A USB网卡在Win11/Win10驱动安装与故障排查全攻略

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

阅读更多 →
小网站设计多少钱?3个步骤避开90%的建站坑 2026/9/27 4:01:04

小网站设计多少钱?3个步骤避开90%的建站坑

小网站设计多少钱?3个步骤避开90%的建站坑 找建站公司报价时,是不是心里直打鼓?怕报低了质量烂,报高了当冤大头?这行水太深,很多老板被“高端定制”忽悠,最后花了大几千,拿到的却是套皮模板。 其实,做一个靠谱的小网站, 多少钱…

阅读更多 →
网站的二次开发是什么意思?3步搞定被黑挂马与最佳实践 2026/9/27 4:01:04

网站的二次开发是什么意思?3步搞定被黑挂马与最佳实践

网站的二次开发是什么意思?3步搞定被黑挂马与最佳实践 网站突然打开全是赌博广告,后台密码失效,数据被删?别慌,这大概率是被黑挂马了。很多新手站长这时候只会重装系统,结果越修越烂。其实,解决这类问题的核心不在于“修”,而在于“改”。这里说的“…

阅读更多 →
0代码基础做商城网站开发业务全图解步骤 2026/9/27 4:01:04

0代码基础做商城网站开发业务全图解步骤

0代码基础做商城网站开发业务全图解步骤 手里攥着预算,想给河北的老客户上个商城网站,结果一打听开发报价,心里直打鼓。 很多做市场推广的朋友都有这个痛点: 自己不会代码想做网站 ,但外包太贵,自建又没头绪。 别慌,今天把 商城网站开发业务…

阅读更多 →
STM32限位开关可靠设计:硬件滤波、光耦隔离与消抖状态机 2026/9/27 4:00:51

STM32限位开关可靠设计:硬件滤波、光耦隔离与消抖状态机

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

阅读更多 →
第1章,了解乌班图环境,使用浏览器和邮箱 2026/9/27 4:00:51

第1章,了解乌班图环境,使用浏览器和邮箱

专栏导航 上一篇:第1章,了解乌班图环境,打开终端功能 回到目录 下一篇:第1章:开发环境搭建,安装部分编译工具链 本节前言 对于本节所讲解的知识,有可能,你会需要时不时地参考本专…

阅读更多 →

今日资讯

本周资讯

本月资讯

看完文章仍有疑问?

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

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