ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Mybatis游标Cursor查询实战:用TaoToken统一Key跑通流式读取

Mybatis游标Cursor查询实战:用TaoToken统一Key跑通流式读取 1. 百万级数据导出为什么会 OOM从分页查询到 Mybatis Cursor 流式读取先说结论Mybatis Cursor 查询适合百万级数据导出、批量同步、离线报表这类一次要读很多行、但每行处理完就能丢的场景。它和分页查询最大的区别在于分页是查一批、处理一批、再查下一批而 Cursor 是数据库游标一直开着应用端一行一行地拉。前者要反复拼 SQL、维护 offset后者靠 JDBC 的 fetchSize 控制每次网络往返拉多少行。我见过太多项目在导出百万级数据时直接select * from record然后ListRecord接住结果堆内存瞬间飙到几个 GGC 疯狂停顿最后java.lang.OutOfMemoryError: Java heap space。也有人用分页limit 0,10000、limit 10000,10000一路翻下去数据量一大 offset 越翻越慢深分页在 MySQL 上几乎是灾难。Cursor 的思路完全不同。它实现了Closeable和Iterable你可以用迭代器逐条拿数据数据库连接在事务内保持打开结果集不会一次性全部加载到 JVM 堆里。核心接口长这样public interface CursorT extends Closeable, IterableT { boolean isOpen(); // 取数据前判断游标是否打开 boolean isConsumed(); // 判断结果是否全部取完 int getCurrentIndex(); // 已获取多少条 }这篇就围绕Mybatis Cursor 流式查询在百万级数据导出场景下的落地来讲从 Mapper 返回 Cursor到 try-with-resources 关闭再到 fetchSize 与 ResultHandler 的取舍最后给出可复制的配置片段、遍历代码和内存占用对比验证步骤。同时说明怎么用 TaoToken 统一 Key 管理调用凭证避免多工具切换时反复改配置。适合谁看正在做数据导出、批量同步、离线任务的后端同学被 OOM 和深分页折磨过的想搞清楚 fetchSize 到底怎么生效的。下面每一步都能跟着做。2. TaoToken 前置准备统一 Key 管理调用凭证避免多工具反复改配置在正式写 Cursor 代码之前先把调用凭证这件事理顺。很多同学在做数据导出时往往不止一个工具在跑本地 IDE 里调试、CI 里跑批、线上定时任务每个环境一套 Key改来改去很容易出错。TaoToken 的作用就是把这些调用凭证统一管理起来一个 Key 走通多个工具不用每次切环境都去翻配置文件。TaoToken 官网地址是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 。注意 API 地址不带 UTM 参数配置的时候别把查询串带进去否则有些客户端会报签名或路径错误。你需要准备的东西其实就三样Base URL、API Key、Model ID。这三件套在后面的配置片段里会反复出现尤其是用 Claude Code、Cline、Codex 这类工具时缺一个都连不上。获取 Key 的路径是进控制台在 API Keys 页面创建。控制台地址是 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API Keys 页面是 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 。创建完记得复制保存页面刷新后就看不到完整 Key 了。如果你只是想先验证模型能不能通可以用模型对话页面 https://taotoken.net/models?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/doc?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 遇到配置问题先翻文档比到处问快。这里要强调一点TaoToken 是统一管理调用凭证的服务不是让你拿它去替代数据库连接或者编辑器。Mybatis 的 Cursor 查询走的是你自己的 JDBC 连接TaoToken 管的是你在开发、调试、跑批过程中调用模型能力的凭证。两者是配合关系别搞混。把 Key 准备好之后我们进入正题先看 Mybatis 的配置怎么写。3. 可复制配置Mybatis Cursor 的 fetchSize、事务与 Mapper 写法这一节给的都是能直接抄的片段。先看 Mybatis 的核心配置。Cursor 能不能流式生效关键在fetchSize和resultSetType。在 Mybatis 的配置里defaultFetchSize可以设一个默认值但更推荐在具体 Statement 上单独指定因为不同查询的数据量差别很大。下面是一个mybatis-config.xml的片段configuration settings setting namedefaultFetchSize value1000/ setting namedefaultStatementTimeout value300/ setting namemapUnderscoreToCamelCase valuetrue/ /settings /configuration如果你用的是 Spring Boot 的application.yml可以这样配mybatis: configuration: default-fetch-size: 1000 default-statement-timeout: 300 map-underscore-to-camel-case: truefetchSize设成 1000 的意思是JDBC 每次从数据库网络往返拉 1000 行到客户端。注意MySQL 的 JDBC 驱动要真正流式生效必须满足两个条件fetchSize设为Integer.MIN_VALUE或者配合useCursorFetchtrue并且resultSetType是FORWARD_ONLY。很多人配了 fetchSize 却发现内存还是爆就是因为驱动没开 cursor fetch。MySQL 连接串建议这样写jdbc:mysql://127.0.0.1:3306/demo?useCursorFetchtrueuseServerPrepStmtstruerewriteBatchedStatementstrueuseCursorFetchtrue是让驱动用服务端游标useServerPrepStmtstrue配合服务端预处理。这两个一起开fetchSize 才会按你设的值分批拉。然后是 Mapper。返回 Cursor 的方法签名要写对Mapper public interface RecordMapper { Select(select id, name, amount, created_at from record order by id) Options(fetchSize 1000, resultSetType ResultSetType.FORWARD_ONLY) CursorRecord streamAllRecords(); }Options里的fetchSize会覆盖全局默认值resultSetType FORWARD_ONLY保证结果集只能向前读这是流式的前提。如果你用 XML 写 SQL等价写法是select idstreamAllRecords resultTypecom.demo.entity.Record fetchSize1000 resultSetTypeFORWARD_ONLY select id, name, amount, created_at from record order by id /select接下来是业务层。这里有个坑必须提前说Cursor 必须在事务内使用。因为 Cursor 依赖数据库连接保持打开如果方法没有TransactionalMybatis 在查询方法返回后就把连接还回连接池了你再遍历 Cursor 就会报连接已关闭或者取不到数据。Service public class RecordExportService { Autowired private RecordMapper recordMapper; Transactional(readOnly true, timeout 600) public void exportAll() throws Exception { try (CursorRecord cursor recordMapper.streamAllRecords()) { cursor.forEach(record - { // 这里做单行处理写文件、发消息、聚合统计 processOne(record); }); } } private void processOne(Record record) { // 具体处理逻辑 } }try-with-resources保证 Cursor 用完自动关闭即使中间抛异常也会关。Transactional(readOnly true)告诉数据库这是只读事务某些数据库会做优化。timeout 600是事务超时时间百万级导出可能跑几分钟别用默认的短超时。如果你用 Cline 或者 Claude Code 这类工具辅助写代码配置里同样需要 Base URL、API Key、Model ID 三件套。以 Cline 的 MCP 配置为例一个典型的 settings 片段是这样的{ mcpServers: { taotoken: { url: https://taotoken.net/api, headers: { Authorization: Bearer YOUR_API_KEY }, model: YOUR_MODEL_ID } } }注意url用的是https://taotoken.net/api不带任何查询参数。Authorization头里放你的 Keymodel填你在控制台看到的 Model ID。这三样对齐了工具才能正常调用。Codex 的auth.json也是类似结构把 Base URL、Key、Model ID 填进去就行。CC Switch 切换配置时确保这三个字段跟着环境走别只改了 Key 忘了 Model ID。配置部分到这里就齐了。下一节我们实际跑一遍看请求怎么发、结果怎么验证。4. 验证请求与成功结果Cursor 遍历、内存对比与 ResultHandler 取舍配置写完之后先做一个小数据量的验证确认 Cursor 真的在流式读而不是一次性加载。验证分三步先跑通遍历再看内存占用最后对比 ResultHandler。第一步写一个带计数和日志的遍历Transactional(readOnly true, timeout 600) public void exportWithLog() throws Exception { long count 0; long start System.currentTimeMillis(); try (CursorRecord cursor recordMapper.streamAllRecords()) { for (Record record : cursor) { count; if (count % 10000 0) { System.out.println(已处理 count 条当前索引 cursor.getCurrentIndex()); } processOne(record); } } long cost System.currentTimeMillis() - start; System.out.println(总计 count 条耗时 cost ms); }跑起来之后你应该能看到每处理 1 万条打一行日志getCurrentIndex()的值跟着涨。如果日志只在最后一次性刷出来说明没流式生效八成是 fetchSize 或 useCursorFetch 没配对。第二步看内存。在启动参数里加上-Xmx256m故意把堆压小。如果 Cursor 生效百万级数据也能在 256M 堆里跑完如果没生效跑到几十万条就 OOM 了。这是最直观的验证方式。你也可以用jconsole或者jstat -gc pid 1000观察老年代增长流式读取时老年代应该基本平稳不会阶梯式上涨。第三步对比 ResultHandler。Mybatis 的ResultHandler是另一种处理大结果集的方式它不返回 Cursor而是在每行结果上回调Transactional(readOnly true) public void exportWithHandler() { recordMapper.scanAllRecords(context - { Record record context.getResultObject(); processOne(record); }); }对应的 Mapper 方法返回voidSelect(select id, name, amount, created_at from record order by id) Options(fetchSize 1000, resultSetType ResultSetType.FORWARD_ONLY) void scanAllRecords(ResultHandlerRecord handler);两者的取舍是这样的Cursor 更灵活你可以在遍历过程中随时break、可以拿到getCurrentIndex()、可以嵌套其他逻辑适合需要精细控制的场景ResultHandler 更简洁Mybatis 帮你管遍历适合每行处理逻辑固定、不需要中断的场景。但 ResultHandler 有个限制它和某些延迟加载、嵌套查询配合时行为不太一样而且你没法在回调里方便地控制游标状态。实测下来百万级导出我一般优先用 Cursor因为可控性强出问题好排查。ResultHandler 适合那种纯粹的读一行写一行的 ETL。验证成功的标志有三个日志按批次输出、小堆内存不 OOM、getCurrentIndex()单调递增到总数。三个都满足说明流式读取真正跑通了。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth 报错对照这一节把常见的报错列出来对照着排查。注意有些报错来自数据库层有些来自工具调用层别混在一起。报错一java.sql.SQLException: Streaming result set is still active这个通常是因为你在 Cursor 还没关闭的时候又发起了同一个连接上的新查询。解决办法是确保 Cursor 在 try-with-resources 里用完即关别在遍历过程中调用同一个 Mapper 的其他方法。如果确实需要嵌套查询考虑换连接或者先把数据落盘。报错二Connection is closed或取不到数据九成是忘了Transactional。Cursor 依赖事务维持连接方法上没有事务注解查询返回后连接就还回池子了。加上Transactional(readOnly true)即可。另外注意如果你在Transactional方法里又开了新线程去遍历 Cursor也会出问题因为事务和连接是绑定线程的。报错三401 Unauthorized这个一般出现在工具调用层不是数据库层。检查你的 API Key 是否正确、有没有多余空格、Authorization头格式是不是Bearer YOUR_API_KEY。如果用的是 TaoToken去 API Keys 页面重新确认一下 Key 状态。401 基本都是凭证问题跟 Cursor 本身无关。报错四local proxy failed这个报错通常出现在客户端配置了本地转发但目标地址不通的时候。检查你的 Base URL 是不是写成了https://taotoken.net/api有没有误加路径或者查询参数。有些工具会把 Base URL 和完整 endpoint 拼错导致请求发到不存在的路径。确认配置里只有 Base URL具体路径由工具自己拼。报错五reading choices相关报错这类报错一般出现在解析模型返回结构时返回体里没有choices字段说明请求根本没到模型或者返回了错误结构。先确认 Base URL、Key、Model ID 三件套是否齐全再看返回的原始 body 是什么。常见原因是 Model ID 填错或者请求被中间层拦截返回了 HTML 错误页。报错六OAuth相关报错如果你用的是需要 OAuth 的工具报 OAuth 错误通常是 token 过期或者回调地址不匹配。这类问题跟 Mybatis Cursor 无关属于工具链配置问题。检查 token 有效期重新走一遍授权流程。如果工具支持 API Key 模式优先用 Key比 OAuth 少一层折腾。报错七fetchSize 设了但内存还是涨回到第 3 节确认 MySQL 连接串带了useCursorFetchtrue并且resultSetType是FORWARD_ONLY。另外PostgreSQL 的流式需要把 autoCommit 设为 false且 fetchSize 不能为 0。不同数据库驱动行为不一样别拿 MySQL 的配置直接套到 PG 上。排查顺序建议先看是不是事务问题再看驱动配置最后看工具层凭证。数据库层的错和工具层的错分开定位能省很多时间。6. 把 Cursor 流式读取接进你的日常任务凭证统一与长期编码Cursor 跑通之后接下来就是把它接进日常任务。百万级导出、批量同步、离线报表这些场景都可以用同一套模式Mapper 返回 CursorService 层用 try-with-resources 包住事务注解别忘fetchSize 按数据量调。凭证这块如果你同时在用多个工具做开发建议统一走 TaoToken 管理。一个 Key 走通模型对话、编码辅助、跑批脚本不用每个工具配一套。需要验证模型能力时用模型对话页面长期做编码和 Agent 任务时看 Coding Plan接入细节翻文档。这样切换环境时只改一处减少配置漂移。最后留几个实用技巧。第一导出任务尽量在低峰期跑流式读取虽然省内存但长时间持有数据库连接会占用连接池资源连接池大小要留够。第二fetchSize不是越大越好1000 到 5000 之间比较稳太大反而增加单次网络传输压力。第三处理逻辑里如果有远程调用考虑批量聚合后再发别一行一次否则百万次远程调用比 OOM 还慢。第四记得给导出任务加监控记录处理条数和耗时出问题能快速定位是卡在数据库还是卡在业务处理。把这些串起来你的百万级数据导出就能稳定跑在有限内存里凭证管理也不再是负担。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表