批次處理與交易

不重試的簡單批次處理

考慮以下不重試的巢狀批次處理簡單範例。它顯示了批次處理的常見情境:處理輸入來源直到耗盡,並在處理「區塊」結束時定期提交。

1   |  REPEAT(until=exhausted) {
|
2   |    TX {
3   |      REPEAT(size=5) {
3.1 |        input;
3.2 |        output;
|      }
|    }
|
|  }

輸入操作 (3.1) 可能是基於訊息的接收(例如來自 JMS)或基於檔案的讀取,但為了恢復並繼續處理以有機會完成整個 Job,它必須是交易性的。這同樣適用於操作 3.2。它必須是交易性的或具備冪等性。

如果 REPEAT (3) 的區塊由於 3.2 的資料庫例外而失敗,則 TX (2) 必須回滾整個區塊。

簡單無狀態重試

對於非交易性操作使用重試也很有用,例如呼叫 Web 服務或其他遠端資源,如下例所示

0   |  TX {
1   |    input;
1.1 |    output;
2   |    RETRY {
2.1 |      remote access;
|    }
|  }

這實際上是重試最有用的應用之一,因為遠端呼叫比資料庫更新更有可能失敗且可重試。只要遠端存取 (2.1) 最終成功,交易 TX (0) 就會提交。如果遠端存取 (2.1) 最終失敗,則保證交易 TX (0) 會回滾。

典型的重複-重試模式

最典型的批次處理模式是在區塊的內部區塊中新增重試,如下例所示

1   |  REPEAT(until=exhausted, exception=not critical) {
|
2   |    TX {
3   |      REPEAT(size=5) {
|
4   |        RETRY(stateful, exception=deadlock loser) {
4.1 |          input;
5   |        } PROCESS {
5.1 |          output;
6   |        } SKIP and RECOVER {
|          notify;
|        }
|
|      }
|    }
|
|  }

內部 RETRY (4) 區塊標記為「有狀態」。請參閱典型用例以取得有狀態重試的描述。這表示,如果重試 PROCESS (5) 區塊失敗,則 RETRY (4) 的行為如下

  1. 擲出例外,在區塊層級回滾交易 TX (2),並允許將項目重新呈現到輸入佇列。

  2. 當項目重新出現時,可能會根據現有的重試策略重新嘗試,並再次執行 PROCESS (5)。第二次和後續嘗試可能會再次失敗並重新擲出例外。

  3. 最終,項目最後一次重新出現。重試策略不允許再次嘗試,因此永遠不會執行 PROCESS (5)。在這種情況下,我們遵循 RECOVER (6) 路徑,有效地「跳過」已接收且正在處理的項目。

請注意,用於計畫中 RETRY (4) 的符號明確顯示輸入步驟 (4.1) 是重試的一部分。它也清楚地表明有兩個替代的處理路徑:正常情況,如 PROCESS (5) 所示,以及復原路徑,如 RECOVER (6) 在單獨的區塊中所示。這兩個替代路徑完全不同。在正常情況下,只會採用其中一個。

在特殊情況下(例如特殊的 TranscationValidException 類型),重試策略可能能夠判斷在 PROCESS (5) 剛失敗後,可以在最後一次嘗試時採用 RECOVER (6) 路徑,而不是等待項目重新呈現。這不是預設行為,因為它需要詳細了解 PROCESS (5) 區塊內部發生的情況,而這通常是不可用的。例如,如果輸出在失敗之前包含寫入存取權,則應重新擲出例外以確保交易完整性。

外部 REPEAT (1) 中的完成策略對於計畫的成功至關重要。如果輸出 (5.1) 失敗,它可能會擲出例外(通常會這樣做,如所述),在這種情況下,交易 TX (2) 失敗,並且例外可能會透過外部批次 REPEAT (1) 向上傳播。我們不希望整個批次停止,因為如果我們再次嘗試,RETRY (4) 可能仍然會成功,因此我們將 exception=not critical 新增至外部 REPEAT (1)。

但是請注意,如果 TX (2) 失敗,並且由於外部完成策略,我們確實再次嘗試,則在內部 REPEAT (3) 中下一個處理的項目不能保證是剛失敗的項目。它可能是,但這取決於輸入 (4.1) 的實作。因此,輸出 (5.1) 可能會再次在新項目或舊項目上失敗。批次的用戶端不應假設每個 RETRY (4) 嘗試都會處理與上次失敗的項目相同的項目。例如,如果 REPEAT (1) 的終止策略是在 10 次嘗試後失敗,則它會在 10 次連續嘗試後失敗,但不一定在同一個項目上失敗。這與整體重試策略一致。內部 RETRY (4) 知道每個項目的歷史記錄,並且可以決定是否再次嘗試。

非同步區塊處理

可以透過設定外部批次以使用 AsyncTaskExecutor 來並行執行典型範例中的內部批次或區塊。外部批次會等待所有區塊完成後再完成。以下範例顯示非同步區塊處理

1   |  REPEAT(until=exhausted, concurrent, exception=not critical) {
|
2   |    TX {
3   |      REPEAT(size=5) {
|
4   |        RETRY(stateful, exception=deadlock loser) {
4.1 |          input;
5   |        } PROCESS {
|          output;
6   |        } RECOVER {
|          recover;
|        }
|
|      }
|    }
|
|  }

非同步項目處理

典型範例中區塊中的個別項目原則上也可以並行處理。在這種情況下,交易邊界必須移動到個別項目的層級,以便每個交易都在單一執行緒上,如下例所示

1   |  REPEAT(until=exhausted, exception=not critical) {
|
2   |    REPEAT(size=5, concurrent) {
|
3   |      TX {
4   |        RETRY(stateful, exception=deadlock loser) {
4.1 |          input;
5   |        } PROCESS {
|          output;
6   |        } RECOVER {
|          recover;
|        }
|      }
|
|    }
|
|  }

此計畫犧牲了簡單計畫所具有的最佳化優勢,即將所有交易資源區塊化在一起。僅當處理 (5) 的成本遠高於交易管理 (3) 的成本時,此計畫才有用。

批次處理與交易傳播之間的互動

批次重試和交易管理之間的耦合比我們理想中更緊密。特別是,無狀態重試不能用於重試不支援 NESTED 傳播的交易管理員的資料庫操作。

以下範例使用不重複的重試

1   |  TX {
|
1.1 |    input;
2.2 |    database access;
2   |    RETRY {
3   |      TX {
3.1 |        database access;
|      }
|    }
|
|  }

再次,出於相同的原因,即使 RETRY (2) 最終成功,內部交易 TX (3) 也可能導致外部交易 TX (1) 失敗。

不幸的是,如果存在重複批次,則相同的效果會從重試區塊向上滲透到周圍的重複批次,如下例所示

1   |  TX {
|
2   |    REPEAT(size=5) {
2.1 |      input;
2.2 |      database access;
3   |      RETRY {
4   |        TX {
4.1 |          database access;
|        }
|      }
|    }
|
|  }

現在,如果 TX (3) 回滾,則可能會污染 TX (1) 的整個批次,並強制其在結束時回滾。

非預設傳播呢?

  • 在前面的範例中,如果兩個交易最終都成功,則 TX (3) 的 PROPAGATION_REQUIRES_NEW 會防止外部 TX (1) 被污染。但是,如果 TX (3) 提交而 TX (1) 回滾,則 TX (3) 保持提交狀態,因此我們違反了 TX (1) 的交易合約。如果 TX (3) 回滾,則 TX (1) 不一定會回滾(但實際上可能會回滾,因為重試會擲出回滾例外)。

  • TX (3) 的 PROPAGATION_NESTED 在重試案例中(以及對於具有跳過的批次)按我們的要求運作:TX (3) 可以提交,但隨後可以被外部交易 TX (1) 回滾。如果 TX (3) 回滾,則 TX (1) 實際上會回滾。此選項僅在某些平台上可用,不包括 Hibernate 或 JTA,但它是唯一始終有效的選項。

因此,如果重試區塊包含任何資料庫存取,則最好使用 NESTED 模式。

特殊情況:具有正交資源的交易

對於沒有巢狀資料庫交易的簡單情況,預設傳播始終是 OK 的。考慮以下範例,其中 SESSIONTX 不是全域 XA 資源,因此它們的資源是正交的

0   |  SESSION {
1   |    input;
2   |    RETRY {
3   |      TX {
3.1 |        database access;
|      }
|    }
|  }

這裡有一個交易訊息 SESSION (0),但它不參與與 PlatformTransactionManager 的其他交易,因此當 TX (3) 啟動時,它不會傳播。在 RETRY (2) 區塊外部沒有資料庫存取。如果 TX (3) 失敗,然後最終在重試時成功,則 SESSION (0) 可以提交(獨立於 TX 區塊)。這類似於原始的「盡力而為單階段提交」情境。最壞的情況是當 RETRY (2) 成功且 SESSION (0) 無法提交時(例如,由於訊息系統不可用)出現重複訊息。

無狀態重試無法復原

先前顯示的典型範例中,無狀態重試和有狀態重試之間的區別非常重要。實際上,最終是交易約束迫使了這種區別,並且此約束也清楚地說明了這種區別存在的原因。

我們從觀察到,除非我們將項目處理包裝在交易中,否則無法跳過失敗的項目並成功提交區塊的其餘部分。因此,我們將典型的批次執行計畫簡化如下

0   |  REPEAT(until=exhausted) {
|
1   |    TX {
2   |      REPEAT(size=5) {
|
3   |        RETRY(stateless) {
4   |          TX {
4.1 |            input;
4.2 |            database access;
|          }
5   |        } RECOVER {
5.1 |          skip;
|        }
|
|      }
|    }
|
|  }

前面的範例顯示了無狀態 RETRY (3),其 RECOVER (5) 路徑在最後一次嘗試失敗後啟動。stateless 標籤表示區塊重複執行,而不會將任何例外重新擲出到某個限制。這僅在交易 TX (4) 具有巢狀傳播時才有效。

如果內部 TX (4) 具有預設傳播屬性並回滾,則會污染外部 TX (1)。交易管理員假設內部交易已損壞交易資源,因此無法再次使用。

對巢狀傳播的支援非常少見,以至於我們選擇在目前版本的 Spring Batch 中不支援使用無狀態重試進行復原。始終可以透過使用先前顯示的典型模式來達到相同的效果(以重複更多處理為代價)。