附录 A:批处理和事务
A.1.简单批处理,无需重试
考虑以下不重试的嵌套批处理的简单示例。它显示了批处理的一种常见情况:Importing 源被处理到用尽,然后我们在“大块”处理结束时定期提交。
1 | REPEAT(until=exhausted) {
|
2 | TX {
3 | REPEAT(size=5) {
3.1 | input;
3.2 | output;
| }
| }
|
| }
Importing 操作(3.1)可以是基于消息的接收(例如,来自 JMS 的消息),也可以是基于文件的读取,但是要恢复并 continue 处理并有机会完成整个作业,它必须是事务性的。同样适用于 3.2 的操作。它必须是事务性的或幂等的。
如果REPEAT
(3)处的块由于 3.2 上的数据库异常而失败,则TX
(2)必须回滚整个块。
A.2.简单的 Stateless 重试
将重试用于非事务性操作也很有用,例如对 Web 服务或其他远程资源的调用,如以下示例所示:
0 | TX {
1 | input;
1.1 | output;
2 | RETRY {
2.1 | remote access;
| }
| }
实际上,这是重试最有用的应用程序之一,因为远程调用比数据库更新更有可能失败并且可以重试。只要远程访问(2.1)最终成功,事务TX
(0)就会提交。如果远程访问(2.1)最终失败,则保证事务TX
(0)回滚。
A.3.典型的重试模式
最典型的批处理模式是将重试添加到块的内部块,如以下示例所示:
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)的行为如下:
-
抛出异常,在块级别回滚事务
TX
(2),并允许将该 Item 重新呈现到 Importing 队列中。 -
重新出现该 Item 时,可能会根据适当的重试策略对其进行重试,并再次执行
PROCESS
(5)。第二次和随后的尝试可能会再次失败并重新引发异常。 -
最终,该 Item 再次出现在最后一次。重试策略不允许再次尝试,因此永远不会执行
PROCESS
(5)。在这种情况下,我们遵循RECOVER
(6)路径,有效地“跳过”了已接收并正在处理的 Item。
请注意,以上计划中用于RETRY
(4)的符号明确表明 Importing 步骤(4.1)是重试的一部分。还清楚地表明,有两个备用处理路径:用PROCESS
(5)表示的正常情况,和用RECOVER
(6)单独表示的恢复路径。两条替代路径完全不同。正常情况下只服用一次。
在特殊情况下(例如特殊的TranscationValidException
类型),重试策略可能能够确定PROCESS
(5)刚刚失败之后,上一次尝试可以使用RECOVER
(6)路径,而不必 await 该项重新呈现。这不是默认行为,因为它需要详细了解PROCESS
(5)块内发生的情况,通常这是不可用的。例如,如果输出在失败之前包含写访问权限,则应重新抛出异常以确保事务完整性。
外部REPEAT
(1)中的完成 Policy 对于上述计划的成功至关重要。如果输出(5.1)失败,则可能会引发异常(如所描述的,通常会发生),在这种情况下,事务TX
(2)失败,并且该异常可能会通过外部批处理REPEAT
(1)传播。我们不希望整个批次都停止,因为如果再次尝试RETRY
(4)可能仍然成功,所以我们将exception=not critical
添加到外部REPEAT
(1)。
但是请注意,如果TX
(2)失败,并且我们*根据外部完成策略再次尝试,则不能保证在内部REPEAT
(3)中下一步处理的 Item 只是失败了可能是,但取决于 Importing(4.1)的实现。因此,新 Item 或旧 Item 的输出(5.1)可能再次失败。批处理的 Client 不应假定每次RETRY
(4)尝试都将处理与最后一个失败的 Item 相同的 Item。例如,如果REPEAT
(1)的终止策略在 10 次尝试后失败,则在连续 10 次尝试后失败,但不一定在同一 Item 上。这与整体重试策略一致。内部RETRY
(4)知道每个 Item 的历史记录,并可以决定是否要对其进行其他尝试。
A.4.异步块处理
通过将外部批处理配置为使用AsyncTaskExecutor
,可以并行执行typical example中的内部批处理或块。外部批处理在完成之前 await 所有块完成。以下示例显示了异步块处理:
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;
| }
|
| }
| }
|
| }
A.5.异步 Item 处理
原则上,也可以同时处理typical example中块中的各个 Item。在这种情况下,事务边界必须移至单个 Item 的级别,以便每个事务都位于单个线程上,如以下示例所示:
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)的成本比事务 Management(3)的成本高得多时,它才有用。
A.6.批处理与事务传播之间的相互作用
重试和事务 Management 之间的耦合比我们理想的紧密。特别是,Stateless 重试不能用于通过不支持 NESTED 传播的事务 Management 器重试数据库操作。
以下示例使用重试,而无需重复:
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)的事务 Contract。如果TX
(3)回滚,则TX
(1)不一定(但实际上可能会这样做,因为重试会引发回滚异常)。 -
PROPAGATION_NESTED
在TX
(3)会按照我们在重试情况下(对于具有跳过的批处理)的要求工作:TX
(3)可以提交,但随后会被外部事务TX
(1)回滚。如果TX
(3)回滚,则TX
(1)实际上回滚。此选项仅在某些平台上可用,不包括 Hibernate 或 JTA,但它是唯一始终有效的选项。
因此,如果重试块包含任何数据库访问权限,则NESTED
模式最佳。
A.7.特殊情况:使用正交资源进行事务
对于没有嵌套数据库事务的简单情况,默认传播总是可以的。考虑以下示例,其中SESSION
和TX
不是全局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)无法提交(例如,由于消息系统不可用)时,可能发生的最糟糕情况是消息重复。
A.8.Stateless 重试无法恢复
在上述典型示例中,Stateless 重试与有状态重试之间的区别很重要。实际上,最终是强制执行区分的事务性约束,并且该约束也使显而易见为什么存在区分。
我们首先观察到,除非将 Item 处理包装在事务中,否则无法跳过失败的 Item 并成功提交其余的块。因此,我们将典型的批处理执行计划简化如下:
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;
| }
|
| }
| }
|
| }
前面的示例显示了 Stateless 的RETRY
(3)和RECOVER
(5)的路径,该路径在最终尝试失败后立即启动。 stateless
标签表示将重复执行该块,而不会将任何异常重新抛出到某个限制。仅当事务TX
(4)具有传播嵌套时才有效。
如果内部TX
(4)具有默认的传播属性并回滚,则会污染外部TX
(1)。事务 Management 器认为内部事务破坏了事务资源,因此无法再次使用它。
对 NESTED 传播的支持非常罕见,我们选择在当前版本的 Spring Batch 中不支持 Stateless 重试的恢复。通过使用上面的典型模式,始终可以达到相同的效果(以重复进行更多的处理为代价)。