50万条工资代发,如何保证不全量回滚?

假设这样一个真实业务场景:

月底,公司要给 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 的本质,就是把「大事务」拆成「小事务」。

这样即使某条数据失败,也不会影响整个任务。

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

友情链接更多精彩内容