假设这样一个真实业务场景:
月底,公司要给 50 万名员工发工资。
系统从 CSV 文件读取工资数据,然后批量写入银行系统。
流程大致是:
<pre data-start="297" data-end="335" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; visibility: visible;">
读取工资数据 → 校验数据 → 调用银行接口 → 写入数据库
</pre>
问题来了:
如果在处理到 第 490000 条记录 时,突然发现:
<pre data-start="377" data-end="391" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; visibility: visible;">
银行卡号错误
</pre>
此时如果系统采用 传统事务处理方式:
<pre data-start="417" data-end="446" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important; visibility: visible;">
BEGIN 处理 50 万条 COMMIT
</pre>
那么结果会是:
前面 48 万条成功记录也会全部回滚。
整个批处理直接白干。
这在金融、支付、结算等系统中是 绝对不能接受的。
所以企业级系统通常会采用一种特殊处理模式:
Chunk Processing(块级事务)
这正是 Spring Batch 的核心设计。
今天这篇文章,我们就用一个真实案例彻底讲清楚:
Spring Batch 如何处理 50 万数据,并实现部分回滚。
-****01-
**什么是 Spring Batch? **
Spring Batch 是 Spring 官方推出的 企业级批处理框架。
它专门解决:
批量数据处理
数据迁移
ETL
对账系统
报表生成
等问题。
它的核心设计理念:
把大数据拆成小块处理。
Spring Batch 的处理结构非常清晰:
<pre data-start="938" data-end="1044" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Job └── Step └── Chunk ├── Reader ├── Processor └── Writer
</pre>
可以理解为:
| 组件 | 作用 |
|---|---|
| Job | 一个完整批处理任务 |
| Step | Job中的一个处理步骤 |
| Chunk | 每次处理的数据块 |
| Reader | 读取数据 |
| Processor | 数据处理 |
| Writer | 数据写入 |
如果我们设计一个 50万工资代发系统,典型架构如下:
<pre data-start="1221" data-end="1858" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
+------------------+ | Web 控制台 | | Job监控 / 启停 | +--------+---------+ | | +--------v---------+ | Batch Controller | | JobLauncher | +--------+-----------+ | | +--------v--------+ | Spring Batch | | | | Job -> Step | | -> Chunk | | | +--------+--------+ | +--------v--------+ | 数据存储层 | | MySQL / CSV | +-----------------+
</pre>
整体流程:
<pre data-start="1867" data-end="1928" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
CSV文件 ↓ Reader读取 ↓ Processor校验 ↓ Writer写入数据库
</pre>
[图片上传失败...(image-38f7b8-1772617740276)]
-****02-
**为什么需要「部分回滚」? **
假设:
<pre data-start="1958" data-end="1981" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
工资数据 = 500000 条
</pre>
如果使用 传统事务模式
<pre data-start="2000" data-end="2030" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
BEGIN 处理 500000 COMMIT
</pre>
只要有 1条数据失败
结果就是:
<pre data-start="2055" data-end="2067" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
全部回滚
</pre>
这显然不合理。
所以 Spring Batch 使用:
<pre data-start="2099" data-end="2114" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Chunk事务
</pre>
假设我们设置:
<pre data-start="2150" data-end="2174" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
chunkSize = 1000
</pre>
那么处理流程是:
<pre data-start="2186" data-end="2304" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
50万数据 │ ├─ Chunk1 (1-1000) ├─ Chunk2 (1001-2000) ├─ Chunk3 (2001-3000) ├─ ... └─ Chunk500
</pre>
每个 Chunk:
<pre data-start="2317" data-end="2329" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
独立事务
</pre>
流程如下:
<pre data-start="2338" data-end="2389" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
读取1000条 ↓ 处理1000条 ↓ 写入1000条 ↓ 提交事务
</pre>
假设:
<pre data-start="2413" data-end="2439" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Chunk3 2001 - 3000
</pre>
其中
<pre data-start="2445" data-end="2463" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
第2500条数据失败
</pre>
Spring Batch处理流程:
<pre data-start="2484" data-end="2603" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Chunk3开始 ↓ 读取1000条 ↓ 处理 ↓ 第2500条异常 ↓ 回滚Chunk3 ↓ 重新执行 ↓ 重试3次 ↓ 仍失败 ↓ 跳过该记录 ↓ 提交其余999条
</pre>
最终结果:
<pre data-start="2612" data-end="2634" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
成功:499999 失败:1
</pre>
这就是 部分回滚机制。
[图片上传失败...(image-2dbfcc-1772617740275)]
-****03-
实际应用场景
关键配置(Skip + Retry)
核心代码如下:
<pre data-start="2691" data-end="2812" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
.faultTolerant() .skipLimit(100) .skip(IllegalArgumentException.class) .retryLimit(3) .retry(Exception.class)
</pre>
含义:
| 配置 | 作用 |
|---|---|
| skipLimit | 最多跳过多少条 |
| skip | 哪些异常允许跳过 |
| retryLimit | 失败重试次数 |
| retry | 哪些异常可以重试 |
核心处理流程
完整数据流如下:
<pre data-start="2938" data-end="3059" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
CSV文件 │ ▼ FlatFileItemReader │ ▼ SalaryPaymentProcessor │ ▼ JdbcBatchItemWriter │ ▼ MySQL
</pre>
具体步骤:
1 数据读取
<pre data-start="3080" data-end="3106" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
FlatFileItemReader
</pre>
读取 CSV。
2 数据验证
校验:
员工ID
金额范围
银行卡号
示例:
<pre data-start="3166" data-end="3279" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
if (item.getAmount().compareTo(MAX_AMOUNT) >0) { throw new IllegalArgumentException("金额超过限制"); }
</pre>
3 批量写入
<pre data-start="3298" data-end="3329" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
JdbcBatchItemWriter
</pre>
批量插入数据库。
性能优化
当数据达到:
<pre data-start="3364" data-end="3385" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
50万 100万 500万
</pre>
单线程处理就会变慢。
Spring Batch支持 并行处理。
1 多线程处理
<pre data-start="3442" data-end="3503" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Chunk1 -> Thread1 Chunk2 -> Thread2 Chunk3 -> Thread3
</pre>
配置:
<pre data-start="3510" data-end="3570" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
.taskExecutor(taskExecutor()) .throttleLimit(10)
</pre>
2 Partition 分区处理
适合:
<pre data-start="3603" data-end="3618" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
百万级 千万级
</pre>
结构:
<pre data-start="3625" data-end="3717" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Master Step │ ├─ Partition1 ├─ Partition2 ├─ Partition3 └─ Partition4
</pre>
每个分区:
<pre data-start="3726" data-end="3738" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
独立线程
</pre>
Spring Batch 元数据表
Spring Batch 会自动创建一些表:
| 表名 | 作用 |
|---|---|
| BATCH_JOB_INSTANCE | Job实例 |
| BATCH_JOB_EXECUTION | Job执行记录 |
| BATCH_STEP_EXECUTION | Step执行记录 |
| BATCH_JOB_EXECUTION_PARAMS | Job参数 |
这些表可以实现:
Job恢复
Job重启
运行统计
Spring Batch在企业里非常常见:
1 工资代发
<pre data-start="4023" data-end="4051" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
50000员工 chunk = 1000
</pre>
处理50个Chunk。
2 银行对账
<pre data-start="4083" data-end="4097" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
100万交易
</pre>
批量对账。
3 报表生成
<pre data-start="4123" data-end="4135" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
每天凌晨
</pre>
生成:
<pre data-start="4142" data-end="4157" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
T+1交易报表
</pre>
[图片上传失败...(image-45a6ab-1772617740275)]
-****04-****总结
常见问题
Job中途失败怎么办?
Spring Batch支持:
<pre data-start="4209" data-end="4228" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
Job Restart
</pre>
可以:
<pre data-start="4235" data-end="4250" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
从失败位置继续
</pre>
如何重新处理失败数据?
只需要:
<pre data-start="4280" data-end="4306" style="-webkit-tap-highlight-color: rgba(0, 0, 0, 0); margin: 0px; padding: 0px; outline: 0px; max-width: 100%; box-sizing: border-box !important; overflow-wrap: break-word !important;">
查询 status = FAILED
</pre>
修正后重新执行。
如果你需要处理 几十万甚至百万数据,
Spring Batch几乎是最成熟的解决方案。
核心优势:
Chunk事务机制
部分回滚
失败重试
跳过策略
任务重启
并行处理
Spring Batch 的本质,就是把「大事务」拆成「小事务」。
这样即使某条数据失败,也不会影响整个任务。