50萬條工資代發(fā),如何保證不全量回滾?

假設(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ù)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容