ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

Spring Batch ItemReader 读数据库:JdbcPagingItemReader 分页配置与 TaoToken 接入实践

Spring Batch ItemReader 读数据库:JdbcPagingItemReader 分页配置与 TaoToken 接入实践 1. 从一次批处理翻车说起为什么 JdbcPagingItemReader 值得单独讲Spring Batch 里读数据库很多人第一反应是JdbcCursorItemReader因为它写起来最省事一条 SQL、一个 RowMapper 就完事。我最早做用户数据同步任务时也是这么干的本地跑 5 条数据一切正常上线后表里 80 万行任务跑了十几分钟内存直接顶到 OOM。原因不复杂——游标方式本质上是把整个ResultSet保持打开状态一条一条往下读数据库连接和结果集在整段 chunk 处理期间都不能释放。数据量一大连接池被占满堆内存也跟着涨。JdbcPagingItemReader解决的就是这个场景它不维持长连接游标而是按pageSize一批一批地发分页 SQL每批读完就释放内存占用稳定在单页数据量级别。适合谁适合做订单对账、用户画像批量刷新、日志归档这类「表大、单条处理慢、不能一次性全捞」的批处理任务。它和游标方式的核心差异在于游标是「一次查询、逐条消费」分页是「多次查询、按页消费」前者省数据库往返但吃内存后者多几次查询但内存可控。这篇我会交付一套可直接复制的JdbcPagingItemReaderBean 配置骨架把分页 SQL、sortKey、parameterValues这几个最容易踩坑的点讲透再补上 TaoToken 统一 Key/API 通道的接入配置和本地验证动作目标是一次跑通分页读取并确认数据条数正确。2. TaoToken 前置准备统一 Key 与 API 通道批处理任务里经常要调用模型做数据清洗、字段补全或者结果校验如果每个任务各自维护一套 Key配置散落、轮换麻烦。TaoToken 在这里的角色是提供一个统一的 API 通道把模型调用收敛到一个入口批处理代码里只需要读一个环境变量。你需要先拿到一个可用的 Key。登录官网 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content 进入控制台后创建 API Key具体入口在 https://taotoken.net/console?utm_sourcetaotoken_aicg_blog_endutm_contentconsoleutm_campaignrewrite 。Key 的管理页面在 https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 建议按任务维度建多个 Key方便单独吊销。接入地址统一用 https://taotoken.net/api 注意这个地址不带任何查询参数。配置上我习惯用环境变量注入避免硬编码进代码仓库export TAOTOKEN_API_KEYsk-你的key export TAOTOKEN_BASE_URLhttps://taotoken.net/api如果你用的是 Spring Boot可以在application.yml里这样引用taotoken: base-url: ${TAOTOKEN_BASE_URL:https://taotoken.net/api} api-key: ${TAOTOKEN_API_KEY}注意Key 只放在环境变量或配置中心不要提交到 Git。批处理任务通常跑在服务器上环境变量是最省事的注入方式。模型对话的调试入口在 https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 接入文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 。如果你后面要做长期编码或 Agent 类任务可以看 Coding Planhttps://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。3. 可复制配置JdbcPagingItemReader Bean 骨架与分页 SQL先建表和数据方便你本地直接跑CREATE TABLE user ( id bigint NOT NULL AUTO_INCREMENT COMMENT 主键, name varchar(255) DEFAULT NULL COMMENT 用户名, age int DEFAULT NULL COMMENT 年龄, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb3; INSERT INTO user VALUES (1, dafei, 18); INSERT INTO user VALUES (2, xiaofei, 17); INSERT INTO user VALUES (3, zhongfei, 16); INSERT INTO user VALUES (4, laofei, 15); INSERT INTO user VALUES (5, feifei, 14);实体和 RowMapper 是基础件先备好Getter Setter ToString public class User { private Long id; private String name; private int age; } public class UserRowMapper implements RowMapperUser { Override public User mapRow(ResultSet rs, int rowNum) throws SQLException { User user new User(); user.setId(rs.getLong(id)); user.setName(rs.getString(name)); user.setAge(rs.getInt(age)); return user; } }核心是PagingQueryProvider和JdbcPagingItemReader两个 Bean。分页 SQL 不是让你手写limit而是拆成selectClause、fromClause、whereClause、sortKey四段交给框架拼Configuration EnableBatchProcessing public class PageDBReaderJob { Autowired private JobBuilderFactory jobBuilderFactory; Autowired private StepBuilderFactory stepBuilderFactory; Autowired private DataSource dataSource; Bean public UserRowMapper userRowMapper() { return new UserRowMapper(); } Bean public PagingQueryProvider pagingQueryProvider() throws Exception { SqlPagingQueryProviderFactoryBean factoryBean new SqlPagingQueryProviderFactoryBean(); factoryBean.setDataSource(dataSource); factoryBean.setSelectClause(select id, name, age); factoryBean.setFromClause(from user); factoryBean.setWhereClause(where age :age); factoryBean.setSortKey(id); return factoryBean.getObject(); } Bean public JdbcPagingItemReaderUser userItemReader() throws Exception { MapString, Object param new HashMap(); param.put(age, 16); return new JdbcPagingItemReaderBuilderUser() .name(userPagingItemReader) .dataSource(dataSource) .queryProvider(pagingQueryProvider()) .parameterValues(param) .pageSize(2) .rowMapper(userRowMapper()) .build(); } Bean public ItemWriterUser itemWriter() { return items - items.forEach(System.err::println); } Bean public Step step() throws Exception { return stepBuilderFactory.get(step1) .User, Userchunk(2) .reader(userItemReader()) .writer(itemWriter()) .build(); } Bean public Job job() throws Exception { return jobBuilderFactory.get(page-db-reader-job) .start(step()) .build(); } public static void main(String[] args) { SpringApplication.run(PageDBReaderJob.class, args); } }几个参数必须说清楚。sortKey是分页的锚点框架靠它生成where id ? order by id这类翻页条件所以它必须是唯一且稳定的列用主键最稳。pageSize是每页条数不是 chunk 大小两者可以不同pageSize控制单次查询量chunk控制事务提交粒度。parameterValues里的 key 要和whereClause里的:age占位符名字对上对不上会直接报参数缺失。selectClause建议显式列出字段而不是select *一是减少网络传输二是避免表结构变更导致 RowMapper 映射错位。SqlPagingQueryProviderFactoryBean会根据 DataSource 自动识别数据库类型MySQL 生成limitOracle 生成rownum你不用手写方言。4. 验证请求跑通分页读取并确认条数配置写完直接运行main方法。上面数据里age 16的有 3 条18、17、16 对应的三条pageSize2所以应该分两页读第一页 2 条第二页 1 条。控制台输出类似User(id1, namedafei, age18) User(id2, namexiaofei, age17) User(id3, namezhongfei, age16)如果你在 writer 里加了计数最终write被调用两次累计 3 条说明分页逻辑正确。想更直观地看分页 SQL把日志级别调到 DEBUGlogging: level: org.springframework.jdbc.core.JdbcTemplate: DEBUG你会看到框架实际执行的两条 SQL第一条带limit 2第二条带id 2和limit 2这就是sortKey在起作用。批处理任务里如果还要调模型做数据校验可以在 writer 里加一段调用用前面配好的 TaoToken 通道Bean public ItemWriterUser itemWriter(RestTemplate restTemplate, Value(${taotoken.api-key}) String apiKey) { return items - { for (User user : items) { HttpHeaders headers new HttpHeaders(); headers.setBearerAuth(apiKey); headers.setContentType(MediaType.APPLICATION_JSON); // 构造请求体调用 https://taotoken.net/api 下的对话接口 // 这里只演示通道接入具体模型名按文档填 } }; }验证模型通道是否通可以直接用模型对话页面发一条测试消息https://taotoken.net/models?utm_sourcetaotoken_aicg_blog_endutm_contentmodelsutm_campaignrewrite 。批处理里调用失败不要影响主流程建议加 try-catch 并记录日志避免一条数据校验失败导致整个 chunk 回滚。5. 本篇常见错排查报错一sortKey未设置或不是唯一列。现象是任务启动就抛IllegalArgumentException提示 sort key 相关。原因是分页翻页依赖排序锚点如果sortKey有重复值翻页会漏数据或重复读。解决用主键或唯一索引列做sortKey。报错二parameterValues的 key 和whereClause占位符不匹配。现象是运行时报参数绑定异常。检查whereClause里写的是:ageparam.put的 key 也必须是age大小写敏感。报错三selectClause用了select *但 RowMapper 按列名取值。表结构一变就映射错位。解决显式列出字段和 RowMapper 里的列名一一对应。报错四pageSize设得比 chunk 大很多。现象是内存又涨上去了。pageSize决定单次查询返回量设太大等于把分页优势抵消了。一般pageSize和chunk保持同量级或者pageSize略大。报错五DataSource 没注入或指向了错误的库。现象是查不到数据但不报错。检查SqlPagingQueryProviderFactoryBean和JdbcPagingItemReaderBuilder用的是同一个dataSource。报错六TaoToken 调用返回 401。检查环境变量TAOTOKEN_API_KEY是否生效Key 是否被吊销。Key 管理入口https://taotoken.net/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。6. 接入与排障的分流入口如果你卡在 Key 申请、通道配置或者批处理里调用模型报错优先看 API Keys 页面和接入文档https://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 类批处理任务Coding Plan 的配额和通道更适合https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 。最后补一个我踩过的坑JdbcPagingItemReader默认不是线程安全的如果你开了多线程 Step每个线程需要独立的 reader 实例别共用一个 Bean。分页读取本身是顺序翻页的多线程加速要靠分区Partitioner把数据按范围切开每个分区各自分页这个后面单独讲。
返回列表