
1. 百万级数据导出把内存打爆MyBatis Cursor 流式查询到底解决什么问题做数据导出或者批量对账的时候很多人第一反应是selectList一把梭结果本地跑得好好的一上生产就 OOM。原因不复杂MyBatis 默认会把整个ResultSet映射成ListT塞进堆内存1000 万行数据哪怕每行只有几百字节累计起来也是几个 G 的占用JVM 还没开始处理业务逻辑就已经被压垮了。MyBatis Cursor 流式查询就是冲着这个场景来的。它返回的不是一个集合而是一个迭代器CursorT应用每次从迭代器取一条结果数据库连接保持打开结果集按需拉取。这样内存里始终只有当前处理的那一条或一小批数据堆占用从「全量」降到「常数级」。适合谁用做大数据量导出、逐行清洗、批量写文件、对账核销这类「读多写少、单条处理」的后端同学。不适合谁需要随机访问、需要多次遍历同一结果集的场景因为 Cursor 是一次性的取完就没了。我试过在一个对账任务里把selectList换成Cursor同样的 800 万行数据原来堆峰值 3.2G 直接 OOM换完之后稳定在 200M 以内任务从「跑不完」变成「跑得稳」。但这里有个前提流式查询期间数据库连接是占着的事务边界没处理好连接池分分钟被耗尽。所以这篇不只是讲怎么配 Cursor还要把事务、fetchSize、连接释放这几件事串起来讲清楚最后再演示怎么把数据库连接和模型调用统一到 TaoToken 的 Key/API 通道让整个链路只有一个凭证要管。核心检索词先摆出来MyBatis Cursor 流式查询是一种让查询结果以迭代器方式逐条返回的机制能显著降低大数据量场景下的内存占用适合逐行处理与批量导出。下面从配置到验证一步步来。2. 接入前的准备TaoToken 统一 Key 与 MyBatis 环境怎么摆在写 Cursor 配置之前先把「凭证」这件事理顺。很多团队的问题是数据库连接一套账号密码模型调用又是另一套 Key散落在各个配置文件里换环境就得改一堆地方。TaoToken 的思路是把模型调用统一到一个 API 通道上Base URL 固定为https://taotoken.net/api你只需要在控制台生成一个 Key所有走 OpenAI 兼容协议的地方都用这一个 Key。先做前置准备。第一步去 TaoToken 控制台创建 API Key地址是https://taotoken.net/api-keys登录后在密钥管理页新建一个复制出来形如sk-xxxxxxxx的字符串先存到环境变量里别硬编码进代码。第二步确认你的项目里 MyBatis 版本不低于 3.4.0因为 Cursor 是 3.4.0 才引入的。用 Maven 的话看一眼pom.xmldependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.13/version /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version3.0.3/version /dependency第三步把 TaoToken 的接入信息写进配置。如果你用的是 Spring Boot可以在application.yml里加一段模型调用的配置Base URL 指向https://taotoken.net/apiKey 从环境变量读taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model: gpt-4o-mini这里要强调一点TaoToken 是模型调用的统一通道不是数据库连接池别把 JDBC 的 URL 和它混在一起。数据库连接还是走你自己的 MySQL/PostgreSQL 配置TaoToken 负责的是「处理完数据之后要调模型做摘要/分类/清洗」这一步。两者通过同一个 Key 体系管理运维上少一份凭证要轮换。如果你用的是 Claude Code 或者 Cline 这类编码工具想让它们也走同一个通道可以在工具里配置 Base URL 为https://taotoken.net/apiKey 填刚才生成的那个。Cline 的 MCP 配置里需要写全三件套Base URL、API Key、Model ID缺一个都连不上。Model ID 按你实际用的模型填比如gpt-4o-mini或者claude-3-5-sonnet具体以控制台模型列表为准。环境摆好之后先别急着写 Cursor用一条最简单的请求验证 Key 是通的。可以用 curl 打一下模型对话接口curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer $TAOTOKEN_API_KEY \ -H Content-Type: application/json \ -d { model: gpt-4o-mini, messages: [{role: user, content: ping}] }返回里有choices字段就说明通道没问题。这一步过了再往下做 Cursor 的数据库部分出问题的时候才能分清是数据库的事还是模型通道的事。3. 可复制的 Cursor 配置XML、注解、fetchSize 与事务边界这一节是核心把 Cursor 的三种写法都过一遍再讲 fetchSize 和事务这两个最容易踩坑的点。先说 XML 写法。Mapper 接口里方法返回值声明成CursorTpublic interface AppUserMapper { CursorAppUser streamAllUsers(); }对应的 XML 里resultType照常写select标签不需要特殊属性select idstreamAllUsers resultTypecom.example.entity.AppUser SELECT id, name, phone, created_at FROM app_user WHERE status 1 ORDER BY id /select注意这里没有fetchSize属性MyBatis 的 fetchSize 是在Statement层面设的XML 里可以通过fetchSize属性传但更推荐在全局配置或方法参数里控制。注解写法更直接Select(SELECT id, name, phone, created_at FROM app_user WHERE status 1 ORDER BY id) Options(fetchSize 1000, resultSetType ResultSetType.FORWARD_ONLY) CursorAppUser streamAllUsersByAnnotation();fetchSize是关键参数。它告诉 JDBC 驱动每次从数据库拉多少行到客户端。MySQL 的默认行为是把整个结果集拉到内存所以必须配合fetchSize Integer.MIN_VALUE才能触发真正的流式读取或者用useCursorFetchtrue加正数 fetchSize。PostgreSQL 则要求连接处于事务中且autoCommitfalsefetchSize 才生效。这块差异很大下面用表格对照数据库触发流式的条件推荐 fetchSize注意事项MySQLuseCursorFetchtrue且 fetchSize0或 fetchSizeInteger.MIN_VALUE1000 或 MIN_VALUEMIN_VALUE 模式不支持游标前后移动PostgreSQLautoCommitfalse 且 fetchSize01000必须在事务内否则驱动忽略 fetchSizeOraclefetchSize0 即可1000默认已支持无需额外 URL 参数事务边界是第二个大坑。流式查询期间连接必须保持打开所以方法上要加Transactional而且这个注解只在外部调用时生效。如果你在同一个类里 A 方法调 B 方法B 上有TransactionalSpring 的代理不会介入事务不生效Cursor 取到一半连接可能就被回收了。正确做法是把流式查询方法放到独立的 Service 里由外部注入调用。Service public class UserExportService { Autowired private AppUserMapper appUserMapper; Transactional(readOnly true) public void exportToFile(Writer writer) throws IOException { try (CursorAppUser cursor appUserMapper.streamAllUsers()) { IteratorAppUser it cursor.iterator(); while (it.hasNext()) { AppUser user it.next(); writer.write(user.toCsvLine()); writer.write(\n); } } } }try-with-resources保证 Cursor 关闭Transactional保证连接在迭代期间不被释放。readOnly true是优化项告诉数据库这是只读事务某些驱动会走更快的路径。如果你处理完数据还要调模型比如把每行数据丢给模型做分类那模型调用要放在事务外面别让 HTTP 请求占着数据库连接。可以先把数据攒成小批事务提交后再批量调 TaoToken 的接口。4. 验证请求与成功结果从 Cursor 取数到模型调用跑通配置写完得验证两件事Cursor 是不是真的流式取数模型通道是不是真的通。先验证 Cursor。写一个测试方法打印每次取数的耗时和内存占用Test Transactional(readOnly true) public void testCursorStream() { long start System.currentTimeMillis(); AtomicInteger count new AtomicInteger(); try (CursorAppUser cursor appUserMapper.streamAllUsers()) { cursor.forEach(user - { count.incrementAndGet(); if (count.get() % 100000 0) { System.out.println(已处理 count.get() 行耗时 (System.currentTimeMillis() - start) ms); } }); } System.out.println(总计 count.get() 行); }跑起来之后你会看到每处理 10 万行打印一次进度内存曲线是平的不会随行数增长。如果内存还是往上涨八成是 fetchSize 没生效回去检查数据库 URL 参数。MySQL 的 URL 要带上useCursorFetchtruespring.datasource.urljdbc:mysql://localhost:3306/demo?useCursorFetchtrueuseServerPrepStmtstrueuseServerPrepStmtstrue是配合useCursorFetch用的少了它游标可能不生效。PostgreSQL 则确认autoCommit是 falseSpring 的Transactional会自动处理。Cursor 验证通过后接模型调用。假设你要把每行用户数据丢给模型做标签分类可以在事务外批量调public void classifyUsers(ListAppUser batch) { String prompt batch.stream() .map(u - u.getName() : u.getPhone()) .collect(Collectors.joining(\n)); // 调用 TaoToken 模型对话接口 // POST https://taotoken.net/api/v1/chat/completions // Header: Authorization: Bearer ${TAOTOKEN_API_KEY} // Body: {model:gpt-4o-mini,messages:[{role:user,content:prompt}]} }成功的结果是Cursor 逐行取数不涨内存攒够 500 行提交一次模型调用返回的choices[0].message.content里有分类结果。整个链路只有一个 Key 要管数据库连接和模型通道各司其职。如果你想先在网页上试试模型返回长什么样可以去模型对话页面手动发一条确认格式对了再写进代码。5. 常见报错排查401、local proxy failed、reading choices、OAuth 逐个拆流式查询加模型调用报错集中在几个地方逐个说。401 Unauthorized。这个基本是 Key 的问题。先确认环境变量TAOTOKEN_API_KEY真的被读到了echo $TAOTOKEN_API_KEY看一眼。如果 Key 是对的还报 401检查请求头是不是写成了Authorization: Bearer sk-xxx少个空格或者多个换行都会挂。还有一种情况是 Key 被禁用或过期去控制台https://taotoken.net/api-keys重新生成一个。local proxy failed。这个报错通常出现在你本地配了代理但代理没起来或者端口不对。TaoToken 的 API 地址是https://taotoken.net/api直连即可不需要额外代理配置。如果你在代码里或者环境变量里设了HTTP_PROXY、HTTPS_PROXY先清掉再试。Cline 或 Claude Code 里如果配了自定义代理也会报这个检查工具的网络设置。reading choices 相关报错。比如Cannot read properties of undefined (reading choices)意思是返回体里没有choices字段通常是请求根本没成功返回的是错误 JSON。打印完整响应体看一眼常见原因是 model ID 写错了或者请求体格式不对。模型 ID 要以控制台列表为准别自己猜。请求体里messages必须是数组role和content都不能少。OAuth 相关报错。如果你用的是 Claude Code 这类工具它可能默认走 OAuth 登录而不是 API Key。要切到 API Key 模式在工具配置里把认证方式改成 KeyBase URL 填https://taotoken.net/api然后填上你的 Key。Codex 的auth.json里也要对应改确保api_key字段有值base_url指向正确地址。三件套 Base URL、Key、Model ID 缺一不可少一个就连不上。Cursor 取数取到一半断了。这个多半是事务边界问题。检查流式查询方法是不是被同类内部调用Transactional没生效。或者连接池的maxLifetime比查询时间短连接被池子回收了。把maxLifetime调大或者确保查询在独立事务里跑完。fetchSize 不生效内存还是涨。MySQL 确认 URL 带useCursorFetchtrueuseServerPrepStmtstruePostgreSQL 确认autoCommitfalse。还有一个隐蔽的点如果你在 Cursor 上做了collect或者toList那等于把流式又变回全量了内存照样爆。流式查询必须用iterator逐条取别中途收集。6. 把数据库与模型调用统一到一个 Key长期编码与 Agent 场景的落地建议Cursor 流式查询解决的是「数据怎么低内存地读出来」TaoToken 解决的是「读出来之后调模型怎么统一管凭证」。两件事合在一起才是完整的大数据量处理链路。对于长期做数据管道、批量清洗、Agent 自动化的团队建议把模型调用的 Base URL 固定成https://taotoken.net/apiKey 走环境变量注入别散落在代码里。如果你要跑长期的编码任务或者 Agent 流程可以了解一下 Coding Plan它适合需要持续调用模型、按量计费的场景比每次手动管 Key 省事。接入文档在https://taotoken.net/doc里面有各语言的示例照着改 Base URL 和 Key 就能跑。验证模型返回格式的话模型对话页面可以直接试不用写代码。最后给一个实操建议Cursor 的try-with-resources一定要写别指望框架帮你关连接。事务方法放独立 Service别同类自调用。fetchSize 按数据库类型调MySQL 用useCursorFetchPostgreSQL 靠事务。模型调用放事务外攒批再发。这四条做到800 万行导出加模型分类的链路就能稳稳跑下来。