Tasklet 处理模型
本章定位:理解 Step 的另一种实现方式 Tasklet——何时用、怎么用、和 Chunk 模式如何选型。
定义与作用
Tasklet 是 Step 的另一种实现方式,通过 Tasklet.execute() 方法执行单一任务逻辑。与 Chunk 模式(Reader → Processor → Writer 循环)不同,Tasklet 在一个方法调用中完成所有工作,返回 RepeatStatus.FINISHED 表示完成,返回 RepeatStatus.CONTINUABLE 表示还有工作要做。
飞翔科技架构师白歌这样区分两种 Step:
"Chunk Step 是'流水线'——数据在传送带上经过每个工位。Tasklet Step 是'一个操作'——清缓存、调存储过程、发邮件。不是所有事情都适合流水线模式。"
核心原理
Tasklet 执行流程
Tasklet vs Chunk Step 对比
| 维度 | Chunk Step | Tasklet Step |
|---|---|---|
| 处理模型 | 流水线:Reader → Processor → Writer | 单体操作:execute() 一次搞定 |
| 数据量 | 任意大小,流式处理 | 通常不处理数据,或处理少量聚合数据 |
| 事务 | 每个 Chunk 一个事务 | 整个 execute() 一个事务 |
| 重启 | 从 ExecutionContext 恢复断点 | 通常不可重启或自定义重启逻辑 |
| 典型场景 | ETL、数据迁移、报表生成 | 文件清理、存储过程调用、初始化脚本、发送通知 |
完整示例
场景一:飞翔科技——每日作业前的临时文件清理
背景:运营部杨英的每日订单报表作业在运行前需要清理昨日生成的临时文件。架构师白歌建议用一个 Tasklet Step 作为 Job 的第一步。
@Component
public class FileCleanupTasklet implements Tasklet {
private static final String TEMP_DIR = "C:/batch/temp/";
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) throws Exception {
File tempDir = new File(TEMP_DIR);
int deletedCount = 0;
if (tempDir.exists()) {
File[] files = tempDir.listFiles();
if (files != null) {
for (File file : files) {
if (file.delete()) {
deletedCount++;
}
}
}
}
System.out.println("清理完成:删除了 " + deletedCount + " 个临时文件");
return RepeatStatus.FINISHED;
}
}
Job 配置:
@Bean
public Job dailyReportJob(JobRepository jobRepository,
Step cleanupStep,
Step processStep,
Step notifyStep) {
return new JobBuilder("dailyReportJob", jobRepository)
.start(cleanupStep) // Step 1: Tasklet——清理临时文件
.next(processStep) // Step 2: Chunk——处理报表数据
.next(notifyStep) // Step 3: Tasklet——发送邮件通知
.build();
}
@Bean
public Step cleanupStep(JobRepository jobRepository,
PlatformTransactionManager tx,
FileCleanupTasklet cleanupTasklet) {
return new StepBuilder("cleanupStep", jobRepository)
.tasklet(cleanupTasklet, tx)
.build();
}
运行结果:
Job: [dailyReportJob] started
Step [cleanupStep]: 清理完成:删除了 12 个临时文件
Step [cleanupStep]: COMPLETED
Step [processStep]: COMPLETED (5000 records)
Step [notifyStep]: 已发送邮件通知到 ops@feixiang.net
Step [notifyStep]: COMPLETED
Job: [dailyReportJob] COMPLETED
操作前后对比:
| 维度 | 操作前(手动清理) | 操作后(Tasklet) |
|---|---|---|
| 执行方式 | 运维李眉每天手动跑脚本 | 作为 Job 第一步自动执行 |
| 可靠性 | 可能忘记清理,磁盘爆满 | 每次都执行,确保磁盘空间 |
| 追踪 | 无日志 | BATCH_STEP_EXECUTION 记录执行状态 |
| 与主流程的关系 | 独立操作,无关联 | Job 的一部分,失败则整个 Job 失败 |
场景二:调用数据库存储过程
背景:飞翔科技财务系统有一个复杂的月度结算存储过程 sp_monthly_settlement,需要传入月份参数。财务部要求每月 1 号凌晨自动执行。
@Component
@StepScope
public class MonthlySettlementTasklet implements Tasklet {
@Value("#{jobParameters['settlementMonth']}")
private String settlementMonth;
private final JdbcTemplate jdbcTemplate;
public MonthlySettlementTasklet(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) {
System.out.println("开始月度结算:" + settlementMonth);
jdbcTemplate.execute(
"CALL sp_monthly_settlement('" + settlementMonth + "')");
System.out.println("月度结算完成:" + settlementMonth);
return RepeatStatus.FINISHED;
}
}
注:@StepScope 确保 settlementMonth 在 Step 启动时才从 JobParameters 中延迟绑定。
运行结果:
Job: [monthlySettlementJob] with params: {settlementMonth=2026-05}
Step [settlementStep]: 开始月度结算:2026-05
Step [settlementStep]: 月度结算完成:2026-05
Step [settlementStep]: COMPLETED
Job: [monthlySettlementJob] COMPLETED
场景三:CONTINUABLE——分页清理大批量过期数据
有时 Tasklet 需要处理的数据量也很大,但不需要 ItemReader/Processor/Writer 的流水线模式。此时可用 RepeatStatus.CONTINUABLE 实现分页执行:
@Component
public class ExpiredDataCleanupTasklet implements Tasklet {
private static final int DELETE_BATCH_SIZE = 1000;
private final JdbcTemplate jdbcTemplate;
public ExpiredDataCleanupTasklet(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) {
int deleted = jdbcTemplate.update(
"DELETE FROM audit_logs WHERE created_at < ? LIMIT ?",
LocalDate.now().minusYears(1), DELETE_BATCH_SIZE);
contribution.incrementWriteCount(deleted);
System.out.println("本批删除:" + deleted + " 条");
// 如果本批删除了数据,说明可能还有更多 → 继续
// 如果本批没有删除数据 → 全部清理完成
return deleted > 0
? RepeatStatus.CONTINUABLE
: RepeatStatus.FINISHED;
}
}
运行输出:
本批删除:1000 条
本批删除:1000 条
本批删除:1000 条
本批删除:423 条
本批删除:0 条 → Step 完成
总删除:3423 条
易错场景与避坑
反例一:用 Tasklet 做大批量数据处理
小崔最初把整个 CSV 导入逻辑写在 Tasklet 里:
// ❌ 错误:用 Tasklet 做 10 万行 CSV 导入——失去了 Chunk 的所有优势
@Component
public class BadCsvImportTasklet implements Tasklet {
@Override
public RepeatStatus execute(StepContribution contribution,
ChunkContext chunkContext) {
List<String> lines = Files.readAllLines(Paths.get("big_file.csv"));
for (String line : lines) {
// ... 处理逻辑
jdbcTemplate.update("INSERT INTO ...", ...); // 逐条 SQL
}
return RepeatStatus.FINISHED;
}
}
问题:
Files.readAllLines()一次性加载全部数据到内存 → 10 万行可能 OOM- 逐条
update()没有 JDBC batch → 性能极差 - 没有事务边界控制 → 全部成功或全部失败,无法控制回滚粒度
- 无法断点续传 → 失败后必须从头再来
正确做法:大数据量处理 → 必须用 Chunk Step。
反例二:CONTINUABLE 死循环
运维工程师李眉某次写了一个 CONTINUABLE 的 Tasklet,忘了确保退出条件:
// ❌ 危险:死循环——永远返回 CONTINUABLE
@Override
public RepeatStatus execute(...) {
doSomething();
return RepeatStatus.CONTINUABLE; // 永远不会 FINISHED!
}
后果:Step 永远不结束,直到 JVM 被 kill。
正确做法:确保每次 execute() 推进进度,且退出条件必然可达。
面试高频考点
Q1:Tasklet Step 和 Chunk Step 的区别和适用场景?
Tasklet 适合单一操作(文件清理、存储过程调用、发送通知),一次性执行完成。Chunk 适合数据流水线处理(ETL、报表生成),通过 Reader → Processor → Writer 循环处理大批量数据。前者是"一个动作",后者是"一条流水线"。
Q2:Tasklet 的 CONTINUABLE 机制如何工作?
当
execute()返回RepeatStatus.CONTINUABLE时,框架会在当前事务提交后再次调用execute()。开发者需要在每次调用中推进进度(如分页删除),并在无更多工作可做时返回FINISHED。每次调用之间的事务边界由 Step 配置决定。
Q3:Tasklet 能实现断点续传吗?
可以,但需要开发者手动实现——通过
ExecutionContext在每次execute()之间保存进度。框架不为 Tasklet 提供自动的状态管理,因为 Tasklet 的内部逻辑对框架是透明的。相比之下,Chunk Step 的断点续传由框架自动处理。
上一章:Chunk 处理模型下一章:Job 详解