news 2026/9/28 4:12:07

MySQL环境下,mybatis使用Cursor:从配置到验证的完整实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
MySQL环境下,mybatis使用Cursor:从配置到验证的完整实践

1. 为什么大结果集查询会把内存打爆

先说结论:MySQL 环境下用 MyBatis 做普通查询,默认会把整个结果集一次性读进 JVM 内存,几百万行数据直接 OOM。Cursor(游标)就是解决这个问题的——它让 JDBC 逐批从服务端拉数据,而不是一口气全拿回来。

我在一个订单对账项目里踩过这个坑:单表 800 万行,用List<Order> selectAll()查出来做遍历,堆内存 4G 直接撑爆,GC 日志全是 Full GC。后来改成 Cursor 流式读取,内存曲线立刻平了,稳定在几百 MB。

Cursor 适合谁?适合需要遍历大结果集做批处理、导出、对账、数据迁移的场景。不适合的场景是:你只需要查几条数据,或者需要把结果集反复随机访问——那种情况用普通 List 更省事。

这里有个关键前提:MySQL 的流式查询不是随便设个fetchSize就生效的。它有一套特定的触发条件,配错了就是"伪 Cursor",数据照样全量加载。下面我把配置骨架、验证方法、常见坑一次讲清楚。

2. TaoToken 前置:把模型对话和编码辅助接进来

在动手改 MyBatis 配置之前,我习惯先把调试辅助工具准备好。排查 Cursor 问题时经常需要问模型"这段 JDBC 源码为什么这样判断",或者让模型帮我 review Mapper 写法。TaoToken 在这里的作用是提供一个统一的模型调用入口,省去到处找 Key 的麻烦。

官网入口:https://taotoken.net/?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite&utm_content=

如果你只是想快速验证某个模型对 MyBatis 源码的理解,直接用模型对话页最方便: https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

如果你在做长期的编码工作,比如反复调试 Cursor 配置、让 Agent 帮你改 Mapper,那 Coding Plan 更合适: https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

需要自己管理调用凭证的话,去控制台和 API Keys 页面: https://taotoken.net/console?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

API 地址统一是https://taotoken.net/api,接入文档在这里: https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

用 Claude Code 做编码辅助的,Anthropic 兼容入口: https://taotoken.net/ClaudeCodeAnthropic?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

这些工具不是必须的,但排查 Cursor 这种涉及 JDBC 底层源码的问题时,有个能随时问的模型确实省时间。

3. 可复制的 MyBatis Cursor 配置骨架

3.1 环境与依赖版本

先确认版本,Cursor 的行为在不同版本间有差异:

组件版本说明
JDK8+Cursor 接口在 JDK8 可用
MyBatis3.5.2+低版本 Cursor 支持不完整
mysql-connector-java8.0.228.x 系列流式判断逻辑一致
MySQL Server5.7 / 8.0都支持流式结果集

Maven 依赖:

<dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.2</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.22</version> </dependency>

3.2 Mapper XML 写法

核心是fetchSize="-2147483648",也就是Integer.MIN_VALUE。这个值不是随便写的,后面会解释原因。

<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <mapper namespace="com.demo.mapper.MenuMapper"> <select id="cursor" resultType="com.demo.entity.Menu" fetchSize="-2147483648"> select menu_id as menuId, menu_name as menuName from t_menu where menu_id &lt; #{menuId} </select> </mapper>

注意fetchSize写在<select>标签上,不是写在全局 settings 里。全局 settings 里没有这个配置项,别找错地方。

3.3 Mapper 接口

返回类型必须是org.apache.ibatis.cursor.Cursor,不能是List:

package com.demo.mapper; import com.demo.entity.Menu; import org.apache.ibatis.cursor.Cursor; public interface MenuMapper { Cursor<Menu> cursor(Integer menuId); }

3.4 Service 层:SqlSession 类型是关键

这一步最容易出错。必须用ExecutorType.REUSE打开 SqlSession,用 BATCH 或 SIMPLE 都会导致结果集被提前关闭。

package com.demo.service; import com.demo.entity.Menu; import com.demo.mapper.MenuMapper; import org.apache.ibatis.cursor.Cursor; import org.apache.ibatis.session.ExecutorType; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Iterator; @Service public class MenuService { @Autowired private SqlSessionFactory sessionFactory; public void streamMenu() { SqlSession sqlSession = sessionFactory.openSession(ExecutorType.REUSE); try { MenuMapper mapper = sqlSession.getMapper(MenuMapper.class); Cursor<Menu> cursor = mapper.cursor(1000000); Iterator<Menu> iterator = cursor.iterator(); while (iterator.hasNext()) { Menu menu = iterator.next(); System.out.println(menu.getMenuId() + " - " + menu.getMenuName()); } } finally { sqlSession.close(); } } }

如果你用 Spring 的@Transactional,注意事务边界。Cursor 必须在同一个 SqlSession 生命周期内消费完,跨事务会报 "Cursor is closed"。

4. 验证请求与成功结果

4.1 为什么必须是 Integer.MIN_VALUE

MySQL Connector/J 判断是否启用流式结果集的逻辑在com.mysql.cj.jdbc.StatementImpl#createStreamingResultSet:

protected boolean createStreamingResultSet() { return ((this.query.getResultType() == Type.FORWARD_ONLY) && (this.resultSetConcurrency == java.sql.ResultSet.CONCUR_READ_ONLY) && (this.query.getResultFetchSize() == Integer.MIN_VALUE)); }

三个条件同时满足才走流式:结果集类型是FORWARD_ONLY、并发模式是CONCUR_READ_ONLY、fetchSize 等于Integer.MIN_VALUE。所以-2147483648不是魔法数字,是 MySQL 驱动约定的开关值。

4.2 验证游标是否真的生效

光看代码跑通不够,要确认数据是分批拉的。两个验证方法:

方法一:看内存曲线。用jconsole或jvisualvm挂到进程上,跑一个百万行查询。如果内存平稳,说明流式生效;如果内存阶梯式上涨到峰值,说明是伪 Cursor。

方法二:加日志看拉取节奏。在遍历循环里每 10000 条打一次时间戳:

int count = 0; while (iterator.hasNext()) { Menu menu = iterator.next(); if (++count % 10000 == 0) { System.out.println("已处理 " + count + " 条, 时间: " + System.currentTimeMillis()); } }

真 Cursor 的时间戳是均匀递增的,伪 Cursor 会在开头卡很久(全量加载),然后瞬间打印完。

4.3 成功结果对照

跑通后你应该看到:

  • 控制台逐条打印菜单数据,不是一次性刷屏
  • 内存占用稳定,不随数据量线性增长
  • 处理 100 万行耗时比普通查询略长(因为网络往返多了),但内存安全

5. 本篇常见错误排查

5.1 报 "Cursor is closed"

原因:SqlSession 被提前关闭,或者用了ExecutorType.BATCH/SIMPLE。

BatchExecutor#doQueryCursor里查询完就关了结果集,SimpleExecutor同理。只有ReuseExecutor不关闭,能持续从服务端取数据。改成ExecutorType.REUSE即可。

5.2 内存还是爆了

检查三处:fetchSize是否写成-2147483648(写成1000无效);resultType是否误写成List;是否在遍历前把 Cursor 转成了 List。

5.3 遍历时报连接超时

流式查询会长时间占用连接,MySQL 的wait_timeout默认 8 小时,一般够用。但如果处理逻辑很慢,建议调大net_write_timeout和net_read_timeout。

5.4 Spring 事务里 Cursor 失效

@Transactional默认用 SIMPLE executor。要么手动开 REUSE 的 SqlSession,要么在事务方法里用SqlSessionTemplate指定 executorType。

注意:Cursor 消费完必须关闭 SqlSession,否则连接泄漏。用 try-finally 包住。

6. 接入与排障资源

Cursor 配置本身不复杂,难的是排查"为什么没生效"。遇到 JDBC 源码层面的疑问,或者需要模型帮你 review Mapper 和 Service 写法,可以用这些入口:

API Keys 管理:https://taotoken.net/api-keys?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

接入文档(含各语言 SDK 示例):https://taotoken.net/doc?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

快速验证模型对源码的理解,用模型对话:https://taotoken.net/models?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

长期做编码调试和 Agent 辅助,用 Coding Plan:https://taotoken.net/coding-plan?utm_source=taotoken_aicg_blog_end&utm_medium=csdn&utm_campaign=rewrite

API 基础地址:https://taotoken.net/api

最后补一个实战技巧:生产环境用 Cursor 时,给遍历循环加个批量提交的节奏控制,比如每 5000 条做一次业务侧落库或缓存写入,避免长时间持有连接。另外 Cursor 不支持RowBounds分页,要分页就自己在 SQL 里加limit,别指望 RowBounds 和 Cursor 混用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 4:12:05

网站程序哪个好完整流程

别被模板坑了!选对网站程序才是安全最佳实践 别再盯着那些花里胡哨的模板网站看脸了,模板太丑不够用只是表面现象,真正让你半夜睡不着觉的是它背后藏着的安全黑洞。很多老板觉得建站就是找个好看的壳子,结果上线没两周就被黑得面目全非,数据全丢,这种教训太常见了。想要网站既好看又稳如老狗,选对网站程序才是硬道理…

作者头像 李华
网站建设 2026/9/28 4:11:59

莆田cms建站模板哪家好?揭秘3类方案报价与避坑指南

莆田cms建站模板哪家好?揭秘3类方案报价与避坑指南 改个需求建站公司拖一周,这种憋屈事儿你是不是也遇到过?明明只是换个Banner图,或者调整一下栏目顺序,对方却让你等上三天五天,理由永远是“排期紧”、“开发忙”。这时候你心里肯定在打鼓:莆田cms建站模板哪家好?其实,选对方案比选对人更重要,尤其…

作者头像 李华
网站建设 2026/9/28 4:11:37

建设网站公司浩森宇特怎么选:5个维度拆解建站成本

建设网站公司浩森宇特怎么选:5个维度拆解建站成本 做网站最怕什么?不是代码写不出来,而是做出来的东西像个“电子废品”。 很多老板找过几家做站的,最后都栽在同一个坑里:模板网站太丑,功能又不够用。看着隔壁同行的官网大气、加载快、还能直接挂购物车,自己掏钱做的却像个九十年代的弹窗广告。这时候焦虑就来了,…

作者头像 李华
网站建设 2026/9/28 4:11:27

SAP ADT MCP 未开放 ABAP read 给 Agent:TaoToken 配置骨架与验证清单

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

作者头像 李华