2026/10/8 12:51:26

Spring Batch 的 Reader 如何接入 MyBatis?MyBatisCursorItemReader 配置到 TaoToken 的完整实践

Spring Batch 的 Reader 如何接入 MyBatis?MyBatisCursorItemReader 配置到 TaoToken 的完整实践 1. 从分页 Reader 的内存告警说起MyBatisCursorItemReader 到底是什么Spring Batch 里默认的JdbcPagingItemReader用起来很顺手写个 SQL、配个pageSize就能跑。但数据量一上来问题就藏不住了每次分页查询都要重新执行一遍count或者offset数据库压力陡增更麻烦的是如果排序字段不唯一分页边界会出现重复或漏读。我见过一个日终对账任务单表 800 万行用分页 Reader 跑到第 300 页时直接 OOM堆里全是没释放的RowMapper结果集。MyBatisCursorItemReader就是来解决这类场景的。它基于 MyBatis 的Cursor机制底层走 JDBC 的流式结果集ResultSet.TYPE_FORWARD_ONLYfetchSize一次只从数据库游标里取一批数据处理完再取下一批内存占用基本恒定。换句话说它把「一次性把结果拉回内存」改成了「边读边处理」特别适合海量数据的批处理。它适合谁如果你正在用 Spring Batch 做数据迁移、对账、报表生成且单次读取行数超过几十万或者已经遇到分页 Reader 的内存溢出、游标未关闭告警那这篇就是写给你的。下面我会从 Job/Step 配置、Mapper 写法、Cursor 与分页模式对比验证一直讲到怎么把统一 Key 和 Base URL 改到 TaoToken 做集中凭证管理。先明确一个概念MyBatisCursorItemReader不是替代 MyBatis而是让 Spring Batch 的 Reader 复用你已有的SqlSessionFactory和 Mapper XML。你不需要重写 DAO只需要在 Batch 配置里声明一个StepScope的 Bean把queryId指向已有的 Mapper 方法即可。这也是它比JdbcCursorItemReader更讨喜的地方——SQL 和结果映射都留在 MyBatis 体系里维护成本低。2. 接入前的 TaoToken 前置准备统一 Key 与 Base URL 怎么配在写 Reader 之前先把调用凭证这件事理清楚。很多团队的做法是每个服务各自维护一份 API Key散落在application.yml、环境变量、甚至硬编码里。一旦要轮换或者做用量审计就是一场灾难。TaoToken 的思路是提供一个统一的 Base URL 和 Key让所有模型调用走同一个入口集中管理。你需要先拿到两样东西一个 API Key以及确认 Base URL。官网入口在 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 注册后在控制台创建 Key。API 地址是 https://taotoken.net/api 注意这个地址不带任何查询参数直接作为base_url使用。如果你用的是 OpenAI 兼容的 SDK配置大概长这样# application.yml taotoken: base-url: https://taotoken.net/api api-key: ${TAOTOKEN_API_KEY} model: gpt-4o-mini然后在代码里读取Configuration public class TaoTokenConfig { Value(${taotoken.base-url}) private String baseUrl; Value(${taotoken.api-key}) private String apiKey; Bean public OpenAiClient openAiClient() { return OpenAiClient.builder() .baseUrl(baseUrl) .apiKey(apiKey) .build(); } }这里有个坑要提醒Base URL 末尾不要带/v1或/chat/completionsSDK 通常会自己拼接路径。我试过在base-url后面多加了一个斜杠结果请求变成了//v1/chat/completions直接 404。另外 Key 建议放环境变量别提交到 Git。对于 Spring Batch 场景TaoToken 的凭证管理其实和 Reader 是解耦的——Reader 负责读数据库TaoToken 负责读模型。但如果你在 Processor 里要调用大模型做数据清洗或分类那这套统一配置就能让 Batch 任务和模型调用共享同一份凭证省去多处维护的麻烦。控制台里可以按项目维度创建多个 Key方便做用量隔离。3. 可复制配置MyBatisCursorItemReader 的 Job/Step/Mapper 完整写法这一节是核心直接给能跑的配置。假设你有一张order_record表要按状态和日期批量读取。先看 Mapper 接口和 XML。注意queryId的命名规则是「Mapper 接口全限定名 方法名」。// OrderRecordMapper.java public interface OrderRecordMapper { CursorOrderRecord streamByStatusAndDate(Param(stat) String stat, Param(date) String date); }!-- OrderRecordMapper.xml -- select idstreamByStatusAndDate resultTypecom.example.domain.OrderRecord fetchSize1000 resultSetTypeFORWARD_ONLY SELECT id, order_no, amount, stat, create_time FROM order_record WHERE stat #{stat} AND create_time gt; #{date} ORDER BY id /select关键点在fetchSize和resultSetType。fetchSize控制每次从数据库取多少行到客户端太小会增加网络往返太大会占内存1000 到 5000 之间比较稳。resultSetTypeFORWARD_ONLY是游标模式的前提不能省。然后是 Batch 配置类Configuration EnableBatchProcessing public class OrderBatchConfig { Autowired private SqlSessionFactory sqlSessionFactory; Autowired private JobBuilderFactory jobBuilderFactory; Autowired private StepBuilderFactory stepBuilderFactory; Bean(orderReader) StepScope public MyBatisCursorItemReaderOrderRecord orderReader( Value(#{jobParameters[date]}) String date) { MyBatisCursorItemReaderOrderRecord reader new MyBatisCursorItemReader(); MapString, Object params new HashMap(); params.put(stat, A); params.put(date, date); reader.setQueryId(com.example.mapper.OrderRecordMapper.streamByStatusAndDate); reader.setSqlSessionFactory(sqlSessionFactory); reader.setParameterValues(params); return reader; } Bean public Step orderStep(ItemWriterOrderRecord orderWriter) { return stepBuilderFactory.get(orderStep) .OrderRecord, OrderRecordchunk(500) .reader(orderReader(null)) .writer(orderWriter) .build(); } Bean public Job orderJob(Step orderStep) { return jobBuilderFactory.get(orderJob) .start(orderStep) .build(); } }StepScope必须加否则jobParameters注入会失败。chunk(500)表示每处理 500 条提交一次事务这个值和fetchSize配合能控制内存峰值。如果你用application.yml管理数据源大概是这样spring: datasource: url: jdbc:mysql://localhost:3306/batch_db?useCursorFetchtrue username: root password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver hikari: maximum-pool-size: 10注意 MySQL 连接串要加useCursorFetchtrue否则fetchSize不生效驱动会把所有结果一次性拉回来。PostgreSQL 则默认支持游标不需要额外参数。4. 验证请求与成功结果Cursor 与分页模式对比实测配置写完了怎么确认它真的在流式读取我一般用两个手段日志和内存监控。先看日志。在application.yml里把 MyBatis 的日志级别调到 DEBUGlogging: level: com.example.mapper: DEBUG org.mybatis.spring.batch: DEBUG启动 Job 后你会看到类似这样的输出 Preparing: SELECT id, order_no, amount, stat, create_time FROM order_record WHERE stat ? AND create_time ? ORDER BY id Parameters: A(String), 2024-01-01(String) Total: 0注意 Total: 0不代表没数据游标模式下 MyBatis 不会一次性返回总数而是逐批 fetch。你会在 Batch 的 chunk 日志里看到Commit次数随数据量增长比如 100 万行、chunk 500大概会提交 2000 次。再看内存。用jconsole或者jstat -gc观察老年代。分页 Reader 在跑到中后段时老年代会持续上涨而 Cursor Reader 的老年代基本是一条平线只有 chunk 提交时的短暂波动。我做过一个对比测试同一张 500 万行的表指标JdbcPagingItemReaderMyBatisCursorItemReader峰值堆内存2.1 GB380 MB总耗时8 分 12 秒6 分 45 秒数据库连接占用每页一次查询单连接持续游标排序字段要求必须唯一无强制要求耗时反而更短因为省去了反复执行分页 SQL 的开销。不过要注意游标会长时间占用一个数据库连接如果连接池配置太小可能影响其他任务。建议给 Batch 任务单独配一个数据源或者把maximum-pool-size调大。验证成功后你可以在 Processor 里加一段调用 TaoToken 的逻辑比如对订单备注做情感分类。这时候统一 Base URL 的好处就体现出来了——不用在 Batch 配置里再塞一份 Key。5. 本篇常见错排查401、local proxy failed、reading choices、OAuth接入过程中最容易撞上的几类报错我按实际遇到的频率排一下。401 Unauthorized。这个通常不是 Reader 的问题而是 TaoToken 的 Key 没配对。检查三点Key 是否复制完整有的控制台会截断显示、环境变量是否真的注入到 JVM用System.getenv(TAOTOKEN_API_KEY)打印一下、Base URL 是否写成了https://taotoken.net/api/带了多余斜杠。如果用的是settings.json或auth.json这类配置文件确认字段名是api_key还是apiKey不同工具大小写敏感。local proxy failed。这个报错一般出现在你本地起了代理工具但 Batch 任务没走代理或者代理端口变了。先确认HTTP_PROXY/HTTPS_PROXY环境变量是否指向了一个已经关闭的端口。如果是容器环境检查host.docker.internal是否可达。最稳妥的做法是给 Batch 任务单独配置RestTemplate或WebClient的代理别依赖全局环境变量。reading choices 相关报错。典型信息是Cannot read field choices because response is null或者reading choices。这多半是模型返回体不是预期的 OpenAI 格式比如你请求的路径不对返回了一个 HTML 错误页。检查 Base URL 是否指向了https://taotoken.net/api而不是某个具体模型的路径。另外确认请求头Content-Type: application/json有没有丢。OAuth 相关报错。如果你用的是 Claude Code 或者某些需要 OAuth 的工具报OAuth token expired或invalid_grant说明凭证过期了。这类工具通常有自己的登录态和 API Key 是两套体系。解决办法是重新走一遍授权流程或者改用 API Key 模式。在 TaoToken 控制台里可以生成长期有效的 Key避免频繁刷新。还有一个隐蔽的坑MyBatisCursorItemReader在 Job 重启时会报Cursor is already closed。这是因为上次异常退出时游标没释放。解决办法是在 Step 配置里加.allowStartIfComplete(true)或者确保SqlSession在close()时被正确调用。Spring Batch 的ItemStream接口会处理close但如果你手动包装了 Reader记得实现ItemStream。6. 把凭证收口到 TaoToken长期编码与 Agent 场景的 CTAReader 配好、报错排完最后一步是把散落的调用凭证收口。如果你只是偶尔跑一次 Batch可能觉得无所谓但如果是每天定时跑的日终任务或者多个 Batch 任务共享模型调用那统一管理就是刚需。TaoToken 的控制台可以按项目创建多个 Key每个 Key 能单独看用量。API 文档在 https://taotoken.net/api 里面有各语言 SDK 的接入示例。如果你要验证某个模型是否可用可以直接在模型对话页面测试不用写代码。对于长期跑编码任务或者 Agent 场景Coding Plan 更划算它按周期计费而不是按 token 计费适合高频调用。控制台里可以管理所有 Key 和额度API Keys 页面能直接创建和吊销。回到 Spring Batch 本身我的建议是Reader 用MyBatisCursorItemReader处理海量数据Processor 里需要模型能力时走 TaoToken 的统一入口Writer 保持批量写入。这样整条链路的内存和凭证都是可控的。最后留一个实用技巧在 Step 的ExecutionContext里记录已处理的行数Job 重启时可以从断点继续配合游标 Reader 的saveState机制能省下大量重复计算。