附录 C.批处理和 Transactions

C.1 简单批处理,无重试

请考虑以下嵌套批处理的简单示例,不进行重试。对于批处理来说,这是一个非常常见的场景,其中输入源被处理直到耗尽,但我们在处理的“块”结束时定期提交。

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

输入操作(3.1)可以是 message-based 接收(e.g. JMS),也可以是 file-based 读取,但要恢复和继续处理并有可能完成整个 job,它必须是 transactional。这同样适用于(3.2)的操作 - 它必须是 transactional 或幂等。

如果 REPEAT(3 处的块由于(3.2)处的数据库 exception 而失败,那么 TX(2 将回滚整个块。

C.2 简单无状态重试

对非 transactional 的操作使用重试也很有用,例如调用 web-service 或其他 remote 资源。例如:

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

这实际上是重试中最有用的应用程序之一,因为 remote 调用比数据库更新更有可能失败并且可以重试。当 remote 访问(2.1)最终成功时,transaction TX(0 将提交.如果 remote 访问(2.1)最终失败,则 transaction TX(0)保证回滚。

C.3 典型的 Repeat-Retry Pattern

最典型的批处理 pattern 是在 Simple Batching example 中向块的内部块添加重试。考虑一下:

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)的行为如下所示。

  • 抛出 exception,在块 level 上回滚 transaction TX(2),并允许 item 为 re-presented 到输入队列。

  • 当 item re-appears 时,可能会重试它,具体取决于重试 policy,再次执行 PROCESS(5)。第二次和后续尝试可能再次失败并重新抛出 exception。

  • 最终 time为最终的 time:重试 policy 禁止另一次尝试,所以 PROCESS(5)永远不会被执行。在这种情况下,我们遵循 RECOVER(6)路径,有效地“跳过”收到并正在处理的 item。

请注意,上面计划中用于 RETRY(4)的符号明确表示输入 step(4.1)是重试的一部分。它还清楚地表明有两个备用的_path 用于处理:正常情况用 PROCESS(5 表示,恢复路径是一个单独的块,RECOVER(6)。两个备用的 paths 是完全不同的:在正常情况下只有一个。

在特殊情况下(e.g. 一个特殊的TranscationValidException类型),重试 policy 可能能够确定在 PROCESS(5 刚失败后的最后一次尝试时可以采用 RECOVER(6)路径,而不是等待 item 为 re-presented。这不是默认行为,因为它需要详细了解 PROCESS(5)块内发生的事情,这通常不可用 - e.g. 如果输出在失败之前包含写访问,那么应该重新抛出 exception 以确保 transactional 完整性。

外部的完成 policy,REPEAT(1)对于上述计划的成功至关重要。如果 output(5.1)失败,它可能会抛出一个 exception(它通常会如所描述的那样),在这种情况下,transaction TX(2 会失败,而 exception 可以通过外部批处理 REPEAT(1 传播.我们不希望整个批次停止,因为如果我们再试一次,)可能仍然会成功,所以我们将 exception=not critical 添加到外部 REPEAT(1)。

但是请注意,如果 TX(2)失败并且我们再次尝试,由于外部完成 policy,下一个在内部 REPEAT(3 中处理的 item 不能保证是刚刚失败的那个.它可能是,但它取决于 input(4.1 的 implementation.因此,output(5.1 可能会在新的 item 或旧的 item 上再次失败.批处理的 client 不应该假定每个 RETRY(4)尝试将处理与最后一个失败的项相同的项。 E.g。如果 REPEAT(1 的终止 policy 在 10 次尝试后失败,它将在连续 10 次尝试后失败,但不一定在同一 item.这与整体重试策略一致:内部 RETRY(4)知道每个 item 的历史,并且可以决定是否再次尝试它。

C.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;
|        }
|
|      }
|    }
|
|  }

C.5 异步 Item 处理

原则上块中的各个项目原则上也可以同时处理。在这种情况下,transaction 边界必须移动到单个 item 的 level,以便每个 transaction 都在一个线程上:

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;
|        }
|      }
|
|    }
|
|  }

该计划牺牲了简单计划所具有的所有 transactional 资源的优化优势。仅当处理(5)的成本远高于 transaction management(3)的成本时才有用。

C.6 批处理和 Transaction 传播之间的交互

batch-retry 和 TX management 之间的耦合比我们理想情况下更紧密。特别是 stateless 重试不能用于使用不支持 NESTED 传播的 transaction manager 重试数据库操作。

对于使用重试而不重复的简单 example,请考虑以下事项:

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

同样,出于同样的原因,内部 transaction TX(3)可能导致外部 transaction TX(1)失败,即使 RETRY(2)最终成功。

不幸的是,相同的效果从重试块渗透到周围的重复批次,如果有的话:

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 处污染整个批次并强制它在结束时回滚。

non-default 传播怎么样?

  • 在最后一个例子 PROPAGATION_REQUIRES_NEW at TX(3)将阻止外部 TX(1)被污染,如果两个 transactions 最终成功。但是如果 TX(3)提交并且 TX(1)回滚,那么 TX(3)会保持提交,所以我们违反了__ 7 的 transaction contract。如果 TX(3)回滚,TX(1)不一定(但它可能在实践中,因为重试将抛出回滚 exception)。

  • PROPAGATION_NESTED at TX(3)在重试的情况下(对于具有跳过的批处理)可以正常工作:TX(3)可以提交,但随后由外部 transaction TX(1 回滚.如果 TX(3)回滚,那么 TX(1)将在实践中回滚。此选项仅在某些平台上可用,e.g. 不是 Hibernate 或 JTA,但它是唯一一致的。

因此,如果重试块包含任何数据库访问,则 NESTED 最佳。

C.7 特例:Transactions with Orthogonal Resources

对于没有嵌套数据库 transactions 的简单情况,默认传播总是正常的。考虑一下(其中 SESSION 和 TX 不是 global XA 资源,因此它们的资源是正交的):

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

这里有一个 transactional 消息 SESSION(0),但它没有参与_11的其他 transactions,因此在 TX(3 启动时不会传播.RETRY(2)块外没有数据库访问权限。如果 TX(3)失败然后最终成功重试,则 SESSION(0)可以提交(它可以独立于 TX 块执行此操作)。这类似于 vanilla“best-efforts-one-phase-commit”场景 - 当 RETRY(2 成功且 SESSION(0)无法提交时,可能发生的最糟糕的消息是 e.g. 因为消息系统不可用。

C.8 无状态重试无法恢复

在典型的 example 中,stateless 和 stateful 重试之间的区别很重要。它实际上最终是一个强制区分的 transactional 约束,这种约束也使得区分存在的原因显而易见。

我们首先观察到,除非我们将 item 处理包装在 transaction 中,否则无法跳过失败并成功提交块的 rest 的 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;
|        }
|
|      }
|    }
|
|  }

这里我们有一个带有 RECOVER(5)路径的 stateless RETRY(3),在最后一次尝试失败后启动。 “stateless”标签仅表示将重复该块而不重新抛出任何 exception 直到某个限制。这仅在 transaction TX(4)传播 NESTED 时有效。

如果 TX(3)具有默认的传播 properties 并且它回滚,它将污染外部 TX(1)。 transaction manager 假定内部 transaction 已损坏 transactional 资源,因此无法再次使用它。

对 NESTED 传播的支持非常少见,我们选择不支持在当前版本的 Spring Batch 中使用 stateless 重试进行恢复。使用上面的典型 pattern 总是可以实现相同的效果(以重复更多处理为代价)。

Updated at: 9 months ago
B.10.索引元数据表的建议Table of content词汇表