[Java][Spring][Database] 主流事務操作 @Transactional & TransactionTemplate

Intro

在 Spring 專案開發中,@Transactional 註解因其高易用性,成為處理資料庫事務(Transaction)的首選方式。
然而,在處理批次資料寫入(Bulk Insert/Update)、大批次資料對帳或長流程業務邏輯時,若習慣性地在方法(Method)層級套用 @Transactional,容易引發「長事務(Long Transaction)」,對資料庫與系統穩定性帶來嚴重隱患:

  • 資料庫鎖定與超時 (Lock Wait Timeout):長事務會長時間持有資料列鎖或間隙鎖(Gap Lock),導致其他併發的線上請求因無法取得鎖而排隊超時。
  • Undo Log 膨脹與效能下滑:未 Commit 的事務會阻止 MySQL (InnoDB) 進行 Undo Log 的清理(Purge),導致膨脹的 Undo Log 鏈大幅拖慢全系統的查詢速度。
  • 失敗代價過高:若在處理上萬筆資料的最後一刻發生異常,整個長事務會強制全數回滾(Rollback),導致先前耗費的 CPU 與 I/O 資源完全浪費。

本文旨在釐清 聲明式事務(@Transactional) 與 編程式事務(TransactionTemplate) 的適用場景,並針對批次處理(Chunk / Batch Process)建立標準的事務控制規範,協助團隊在開發便利性與系統效能間取得最佳平衡。


@Transactional vs TransactionTemplate

評估維度 @Transactional (聲明式) TransactionTemplate (編程式)
控制粒度 方法(Method)層級 區塊(Block)層級
侵入性 無侵入,僅加 Annotation 需注入元件並寫 Lambda
維護性 邏輯乾淨,適合 90% 的日常開發 稍為繁瑣,但對底層掌控度最高
常見地雷 迴圈中使用容易造成 Long Transaction 忘記處理 Lambda 內部的變數作用域

核心觀念與比較 (Core Concepts & Comparison)

  • 優先使用 @Transactional 的場景:
    • 日常 Web REST API 請求:如建立訂單、修改會員資料、進行單次扣款等短暫操作。
    • 原子性要求極高:整個 Method 必須符合 All-or-Nothing,不允許任何中間狀態存留。
  • 優先使用 TransactionTemplate 的場景:
    • 批次資料處理(Bulk/Chunk Insert):需要每 N 筆(如 500、1000 筆)獨立 Commit 的情境。
    • 包含第三方 API 呼叫:避免外部網路延遲導致資料庫 Transaction 掛載過久。
    • 部分成功/可重試邏輯:即使中途出錯,也希望前面已成功的批次能保留在資料庫中。

實踐範例 (Best Practices & Code Examples)

錯誤範例:長事務(Long Transaction)

以下為典型的不當使用案例。在 Method 頭部標註 @Transactional,會導致整個迴圈都在同一個 Transaction 內,直到 Method 結束時才發出 COMMIT:

@Transactional
public void processInBulk(List<Data> dataList) {
    // 舊資料清理(鎖定範圍將持續掛載到 Method 結束)
    repository.deleteByDate(now);

    for (int offset = 0; offset < dataList.size(); offset += 1000) {
        List<Data> chunk = getChunk(dataList, offset, 1000);

        // 雖然呼叫 saveAll,但資料庫並未 COMMIT,記憶體與 Undo Log 持續膨脹
        repository.saveAll(chunk);
    }
    // 離開 Method 前才會發出 1 次 COMMIT
}

正確範例:分批獨立 Commit(Chunk Transaction)

移除方法層級的 @Transactional,改以 TransactionTemplate 精確劃分每一個區塊的事務邊界:

@Service
@RequiredArgsConstructor
public class BatchService {

    private final TransactionTemplate transactionTemplate;
    private final DataRepository repository;

    public void processInBulk(List<Data> dataList) {
        // 1. Delete 操作包入獨立事務,執行完立刻 Commit 並釋放鎖定
        transactionTemplate.executeWithoutResult(status -> {
            repository.deleteByDate(now);
        });

        // 2. 每批 Chunk 獨立事務,執行完立即 Commit
        for (int offset = 0; offset < dataList.size(); offset += 1000) {
            List<Data> chunk = getChunk(dataList, offset, 1000);

            if (!chunk.isEmpty()) {
                transactionTemplate.executeWithoutResult(status -> {
                    repository.saveAll(chunk);
                }); // 離開 Lambda 時即完成 COMMIT
            }
        }
    }
}

Leave a Reply

Your email address will not be published. Required fields are marked *