ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Springboot项目如何实现mybatis的流式查询:TaoToken统一Key接入Cursor分页导出实战

Springboot项目如何实现mybatis的流式查询:TaoToken统一Key接入Cursor分页导出实战 1. 为什么大数据量导出总在半夜炸内存先说结论Springboot MyBatis 做几十万行数据导出时默认的ListT返回方式会把整个结果集一次性拉进 JVM堆内存直接顶到天花板。我见过最典型的一次凌晨两点导出 48 万条工资明细服务直接 OOM重启后任务重跑又炸循环三次。这个场景其实很常见财务月报、订单对账、日志归档、用户行为明细导出。数据量说大不大几十万到几百万说小也不小全量selectList必死分页limit offset又慢得离谱——翻到第 500 页时数据库要扫描前 50 万行再丢掉。MyBatis 的流式查询Cursor就是为这种场景准备的它返回的不是集合而是一个迭代器服务端和数据库保持连接按需逐行拉取处理完一批丢一批内存占用基本恒定。配合SqlSession手动管理事务就能把导出链路的内存峰值压到几十 MB 级别。这篇会从零跑通一条完整链路TaoToken 统一 Key 接入 →settings.json/config.toml配置骨架 →Cursor逐行读取 → 内存占用验证 → 常见报错排查。适合正在做 Springboot 数据导出、被 OOM 折磨过的后端同学也适合想搞懂 MyBatis 流式查询到底怎么落地的人。2. TaoToken 统一 Key 前置准备在写Cursor代码之前先把模型调用通道理顺。很多导出任务不只是查库还要顺带做数据清洗、字段翻译、异常摘要这时候如果每个模块各配一套 Key维护起来很痛苦。TaoToken 的思路是统一 Key 统一 API 通道一个 Key 走完对话、编码、Agent 场景。你需要先拿到 Key入口在这里控制台创建和管理 Keyhttps://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewriteAPI Keys 管理页https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite接入文档配置字段说明https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewriteAPI 基础地址统一用https://taotoken.net/api注意这个地址不带 UTM 参数直接写进配置文件即可。注意Key 只创建一次就够不要每个环境各建一个后面排查问题时容易搞混。建议按「项目名-环境」命名比如export-prod。拿到 Key 后先确认两件事一是 Key 有对应模型的调用权限二是本地网络能正常访问 API 地址。这两步没问题再往下写配置。3. 可复制配置settings.json 与 config.toml 骨架配置分两块一块是给编辑器/Agent 用的settings.json一块是给命令行工具用的config.toml。两者都指向同一个 Key 和同一个 API 地址改一处即可。3.1 settings.json 配置骨架{ aiProvider: { name: taotoken, baseUrl: https://taotoken.net/api, apiKey: sk-你的Key填这里, defaultModel: claude-sonnet-4-20250514, timeout: 60000, maxRetries: 3 }, exportTask: { batchSize: 1000, fetchSize: 1000, enableStreamQuery: true } }batchSize和fetchSize建议保持一致都设成 1000。fetchSize是 JDBC 层每次从数据库拉取的行数设太小会增加网络往返设太大又失去流式的意义。1000 是实测下来比较稳的值。3.2 config.toml 配置骨架[provider] name taotoken base_url https://taotoken.net/api api_key sk-你的Key填这里 default_model claude-sonnet-4-20250514 [provider.retry] max_attempts 3 backoff_ms 500 [export] batch_size 1000 fetch_size 1000 stream_query true3.3 MyBatis 流式查询核心配置光有模型配置还不够MyBatis 这边要开两个关键参数。在application.yml里加上mybatis: configuration: default-fetch-size: 1000 default-statement-timeout: 3600 map-underscore-to-camel-case: truedefault-fetch-size是全局默认值但流式查询必须在 Mapper 的select上显式指定fetchSize否则 MySQL 驱动可能仍然一次性加载。default-statement-timeout设大一点导出任务跑几分钟很正常别让它中途超时。提示MySQL 要真正启用流式读取连接串上必须带useCursorFetchtrue否则fetchSize不生效。PostgreSQL 则要关掉自动提交这个后面排障章节会细说。4. Cursor 逐行读取与内存验证配置就绪进入核心代码。整个链路分四步Mapper 返回Cursor→ Service 手动开SqlSession→ 分批消费 → 提交并关闭。4.1 Mapper 层改造原来返回ListPerson的接口改成返回CursorPersonSQL 上加fetchSizeMapper public interface PersonDao { CursorPerson selectByCursor(); Integer queryCount(); }select idselectByCursor resultMappersonMap fetchSize1000 select * from sys_person order by id desc /select select idqueryCount resultTypejava.lang.Integer select count(*) from sys_person /select注意fetchSize写在select标签上不是写在全局配置里就万事大吉。这一步漏了后面内存照样爆。4.2 Service 层手动管理 SqlSession流式查询的关键是在数据读完之前不能提交事务也不能关闭 SqlSession。所以不能用Transactional自动管理得手动来。Service Slf4j public class PersonExportService { Autowired private SqlSessionFactory sqlSessionFactory; public void exportByCursor() { SqlSession sqlSession sqlSessionFactory.openSession(); try { PersonDao mapper sqlSession.getMapper(PersonDao.class); CursorPerson cursor mapper.selectByCursor(); Integer total mapper.queryCount(); ListPerson batch new ArrayList(1000); int batchNo 0; for (Person person : cursor) { batch.add(person); if (batch.size() 1000) { batchNo; processBatch(batch, batchNo); batch.clear(); } } if (!batch.isEmpty()) { batchNo; processBatch(batch, batchNo); batch.clear(); } log.info(导出完成总批次{}总行数{}, batchNo, total); sqlSession.commit(); } catch (Exception e) { log.error(导出异常回滚, e); sqlSession.rollback(); throw new RuntimeException(e); } finally { sqlSession.close(); } } private void processBatch(ListPerson batch, int batchNo) { log.info(处理第 {} 批行数{}, batchNo, batch.size()); // 这里做实际业务写文件、算汇总、调模型清洗字段 } }4.3 内存占用验证动作光说「内存降下来了」没说服力得实测。两个动作第一个动作在processBatch里打印当前堆内存private void processBatch(ListPerson batch, int batchNo) { Runtime rt Runtime.getRuntime(); long usedMB (rt.totalMemory() - rt.freeMemory()) / 1024 / 1024; log.info(第 {} 批行数{}当前堆内存{} MB, batchNo, batch.size(), usedMB); }第二个动作启动参数加上-Xmx256m故意把堆压小。如果流式查询生效256MB 跑 50 万行导出不会 OOM如果没生效跑到几万行就炸了。实测下来50 万行数据、每行约 200 字节流式查询全程堆内存稳定在 80–120MB 之间波动批次处理完立即被 GC 回收。对比全量selectList同样数据量堆内存直接冲到 1.5GB 以上。4.4 连接串关键参数MySQL 连接串必须带这两个参数spring: datasource: url: jdbc:mysql://localhost:3306/test?useCursorFetchtrueuseServerPrepStmtstruerewriteBatchedStatementstrueuseCursorFetchtrue是流式读取的开关useServerPrepStmtstrue配合它使用。少了任何一个fetchSize都会被驱动忽略退化成一次性加载。5. 本篇常见错排查流式查询跑不通八成是下面几个坑。坑一fetchSize设了但没生效内存照样爆。先查连接串有没有useCursorFetchtrue再查select标签上有没有显式写fetchSize。两个都确认了还不行看是不是用了Transactional注解——Spring 的事务代理会在方法返回后才提交但Cursor在方法内就被消费了事务边界和游标生命周期冲突容易出问题。流式查询老老实实手动开SqlSession。坑二Cursor遍历到一半报Connection is closed。这是事务提前提交或SqlSession被关闭导致的。检查代码里有没有在遍历过程中调用sqlSession.commit()或close()。另外如果用了连接池比如 HikariCP确认maxLifetime大于导出任务耗时否则连接被池回收游标直接断。坑三PostgreSQL 下Cursor不生效。PG 需要关闭自动提交才能启用游标读取。在openSession()时传falseSqlSession sqlSession sqlSessionFactory.openSession(false);同时 PG 的fetchSize默认是 0全部加载必须显式设成正数。坑四导出任务跑太久数据库连接被服务端 kill。MySQL 的wait_timeout默认 8 小时一般够用但如果导出任务超过这个时间连接会被服务端断开。解决办法是在连接串加autoReconnecttrue或者把wait_timeout调大。更稳妥的做法是控制单次导出数据量超大批次拆成多个子任务。坑五Cursor和queryCount同时调用报错。同一个SqlSession上Cursor打开后连接被占用再执行queryCount可能报Streaming result set is still active。解决办法是先查总数再开Cursor或者用另一个SqlSession查总数。注意如果导出过程中还要调模型做字段清洗建议把模型调用放在processBatch里每批调一次不要每行调一次。每行调一次 API50 万行就是 50 万次请求延迟和费用都扛不住。6. 接入通道与后续动作链路跑通后把模型调用通道固定下来。TaoToken 的 API 地址统一用https://taotoken.net/apiKey 在控制台管理配置字段参考接入文档。需要管理或新建 Keyhttps://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite配置字段和接入细节https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite想先验证模型通不通https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite长期跑编码/Agent 任务https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite最后留一个实操建议流式查询的batchSize不要拍脑袋定先跑一次导出在processBatch里打印每批耗时和堆内存找到「批次处理时间」和「内存峰值」的平衡点。我试过 500、1000、2000 三档1000 在大多数场景下最稳。如果你的单行数据特别大比如带长文本字段降到 500 更安全。
返回列表