假設(shè)這樣一個真實業(yè)務(wù)場景:
月底,公司要給 50 萬名員工發(fā)工資。
系統(tǒng)從 CSV 文件讀取工資數(shù)據(jù),然后批量寫入銀行系統(tǒng)。
流程大致是:
<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;">
讀取工資數(shù)據(jù) → 校驗數(shù)據(jù) → 調(diào)用銀行接口 → 寫入數(shù)據(jù)庫
</pre>
問題來了:
如果在處理到 第 490000 條記錄 時,突然發(fā)現(xiàn):
<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>
此時如果系統(tǒng)采用 傳統(tǒng)事務(wù)處理方式:
<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>
那么結(jié)果會是:
前面 48 萬條成功記錄也會全部回滾。
整個批處理直接白干。
這在金融、支付、結(jié)算等系統(tǒng)中是 絕對不能接受的。
所以企業(yè)級系統(tǒng)通常會采用一種特殊處理模式:
Chunk Processing(塊級事務(wù))
這正是 Spring Batch 的核心設(shè)計。
今天這篇文章,我們就用一個真實案例徹底講清楚:
Spring Batch 如何處理 50 萬數(shù)據(jù),并實現(xiàn)部分回滾。
-****01-
**什么是 Spring Batch? **
Spring Batch 是 Spring 官方推出的 企業(yè)級批處理框架。
它專門解決:
批量數(shù)據(jù)處理
數(shù)據(jù)遷移
ETL
對賬系統(tǒng)
報表生成
等問題。
它的核心設(shè)計理念:
把大數(shù)據(jù)拆成小塊處理。
Spring Batch 的處理結(jié)構(gòu)非常清晰:
<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 | 一個完整批處理任務(wù) |
| Step | Job中的一個處理步驟 |
| Chunk | 每次處理的數(shù)據(jù)塊 |
| Reader | 讀取數(shù)據(jù) |
| Processor | 數(shù)據(jù)處理 |
| Writer | 數(shù)據(jù)寫入 |
如果我們設(shè)計一個 50萬工資代發(fā)系統(tǒng),典型架構(gòu)如下:
<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監(jiān)控 / 啟停 | +--------+---------+ | | +--------v---------+ | Batch Controller | | JobLauncher | +--------+-----------+ | | +--------v--------+ | Spring Batch | | | | Job -> Step | | -> Chunk | | | +--------+--------+ | +--------v--------+ | 數(shù)據(jù)存儲層 | | 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寫入數(shù)據(jù)庫
</pre>
[圖片上傳失敗...(image-38f7b8-1772617740276)]
-****02-
**為什么需要「部分回滾」? **
假設(shè):
<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;">
工資數(shù)據(jù) = 500000 條
</pre>
如果使用 傳統(tǒng)事務(wù)模式
<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條數(shù)據(jù)失敗
結(jié)果就是:
<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事務(wù)
</pre>
假設(shè)我們設(shè)置:
<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萬數(shù)據(jù) │ ├─ 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;">
獨立事務(wù)
</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條 ↓ 提交事務(wù)
</pre>
假設(shè):
<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條數(shù)據(jù)失敗
</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 ↓ 重新執(zhí)行 ↓ 重試3次 ↓ 仍失敗 ↓ 跳過該記錄 ↓ 提交其余999條
</pre>
最終結(jié)果:
<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 失?。?
</pre>
這就是 部分回滾機制。
[圖片上傳失敗...(image-2dbfcc-1772617740275)]
-****03-
實際應(yīng)用場景
關(guān)鍵配置(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 | 失敗重試次數(shù) |
| retry | 哪些異??梢灾卦?/td> |
核心處理流程
完整數(shù)據(jù)流如下:
<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 數(shù)據(jù)讀取
<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 數(shù)據(jù)驗證
校驗:
員工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>
批量插入數(shù)據(jù)庫。
性能優(yōu)化
當(dāng)數(shù)據(jù)達到:
<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 分區(qū)處理
適合:
<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>
結(jié)構(gòu):
<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>
每個分區(qū):
<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 元數(shù)據(jù)表
Spring Batch 會自動創(chuàng)建一些表:
| 表名 | 作用 |
|---|---|
| BATCH_JOB_INSTANCE | Job實例 |
| BATCH_JOB_EXECUTION | Job執(zhí)行記錄 |
| BATCH_STEP_EXECUTION | Step執(zhí)行記錄 |
| BATCH_JOB_EXECUTION_PARAMS | Job參數(shù) |
這些表可以實現(xiàn):
Job恢復(fù)
Job重啟
運行統(tǒng)計
Spring Batch在企業(yè)里非常常見:
1 工資代發(fā)
<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-****總結(jié)
常見問題
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;">
從失敗位置繼續(xù)
</pre>
如何重新處理失敗數(shù)據(jù)?
只需要:
<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>
修正后重新執(zhí)行。
如果你需要處理 幾十萬甚至百萬數(shù)據(jù),
Spring Batch幾乎是最成熟的解決方案。
核心優(yōu)勢:
Chunk事務(wù)機制
部分回滾
失敗重試
跳過策略
任務(wù)重啟
并行處理
Spring Batch 的本質(zhì),就是把「大事務(wù)」拆成「小事務(wù)」。
這樣即使某條數(shù)據(jù)失敗,也不會影響整個任務(wù)。