批处理和事务
简单批处理,无需重试
请考虑以下没有重试的嵌套批处理的简单示例。它显示了一个 批处理的常见场景:处理输入源直到耗尽,并且 它会在处理的 “chunk” 结束时定期提交。
1 | REPEAT(until=exhausted) { | 2 | TX { 3 | REPEAT(size=5) { 3.1 | input; 3.2 | output; | } | } | | }
输入操作 (3.1) 可以是基于消息的接收(例如来自 JMS)或 基于文件的读取,但要恢复并继续处理,并有机会完成 整个 job,它必须是事务性的。这同样适用于 3.2 中的操作。它必须 可以是 transactional 或 idempotent。
如果 (3) 处的块由于 3.2 处的数据库异常而失败,则 (2)
必须回滚整个 chunk。REPEAT
TX
简单无状态重试
对非事务性操作(如 调用 Web 服务或其他远程资源,如下例所示:
0 | TX { 1 | input; 1.1 | output; 2 | RETRY { 2.1 | remote access; | } | }
这实际上是重试最有用的应用程序之一,因为远程调用是
比数据库更新更有可能失败并且可重试。只要遥控器
Access (2.1) 最终成功,事务 (0) 提交。如果远程
Access (2.1) 最终失败,事务 (0) 保证滚动
返回。TX
TX
典型的重复-重试模式
最典型的批处理模式是将重试添加到 块,如下例所示:
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; | } | | } | } | | }
内部 (4) 块被标记为 “stateful”。有关状态重试的描述,请参阅典型使用案例。这意味着,如果
重试 (5) 块失败,则 (4) 的行为如下:RETRY
PROCESS
RETRY
-
抛出异常,回滚事务 (2),在 chunk 级别,以及 允许将项目重新呈现给 Input 队列。
TX
-
当项目重新出现时,可能会重试,具体取决于现有的重试策略,并且 再次执行 (5)。第二次和后续尝试可能会再次失败,并且 重新引发异常。
PROCESS
-
最终,该项目最后一次重新出现。重试策略不允许另一个 attempt,因此 (5) 永远不会执行。在这种情况下,我们遵循 (6) 路径,有效地 “跳过” 已接收并正在处理的项目。
PROCESS
RECOVER
请注意,计划中用于 (4) 的表示法明确表明
输入步骤 (4.1) 是重试的一部分。它还清楚地表明有两个
处理的替代路径:正常情况,如 (5) 所示,并且
recovery path,如在单独的块中用 (6) 表示的那样。两条备用路径
是完全不同的。在正常情况下,只服用过一次。RETRY
PROCESS
RECOVER
在特殊情况(如特殊类型)中,重试策略
可能能够确定 (6) 路径可以在最后一次尝试时采用
after (5) 刚刚失败,而不是等待项目重新呈现。
这不是默认行为,因为它需要详细了解
发生在 (5) 块内,这通常不可用。例如,如果
失败前的输出包括 write access,异常应该是
rethrown 以确保事务完整性。TranscationValidException
RECOVER
PROCESS
PROCESS
外部 (1) 中的 completion 策略对于
计划。如果输出 (5.1) 失败,它可能会抛出一个异常(通常如此,如
描述),在这种情况下,事务 (2) 会失败,并且异常可能会
通过外部批处理向上传播 (1)。我们不希望整个批次
stop,因为如果我们再试一次,(4) 可能仍然会成功,所以我们添加到外部的 (1) 中。REPEAT
TX
REPEAT
RETRY
exception=not critical
REPEAT
但是请注意,如果 (2) 失败并且我们再次尝试,则凭借外部
completion 策略中,在内部 (3) 中下一个处理的 Item 不是
保证是刚刚失败的那个。可能是,但这取决于
input 的实现 (4.1)。因此,输出 (5.1) 可能会在
新项目或旧项目。批处理的客户端不应假定每个 (4)
尝试将处理与上一个失败的项目相同的项目。例如,如果
(1) 的终止策略是在 10 次尝试后失败,在 10 次尝试后失败
连续尝试,但不一定是针对同一项。这与
总体重试策略。内部 (4) 知道每个项目的历史记录,并且
可以决定是否再次尝试。TX
REPEAT
RETRY
REPEAT
RETRY
异步块处理
典型示例中的内部 batches 或 chunk 可以执行
同时,将外部批处理配置为使用 .外部
batch 等待所有 chunk 完成后再完成。以下示例显示了
异步块处理: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; | } | | } | } | | }
异步项目处理
在典型示例中,chunk 中的单个项目也可以在 原则,同时处理。在这种情况下,事务边界必须移动 添加到单个项的级别,以便每个事务都位于单个线程上,因为 以下示例显示:
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) 的成本。
批处理和事务传播之间的交互
批处理重试和事务管理之间的耦合比我们更紧密 理想情况下是这样的。特别是,无状态重试不能用于重试数据库 操作。
以下示例使用不重复的重试:
1 | TX { | 1.1 | input; 2.2 | database access; 2 | RETRY { 3 | TX { 3.1 | database access; | } | } | | }
同样,出于同样的原因,内部事务 (3) 可以导致外部
transaction (1) 失败,即使 (2) 最终成功。TX
TX
RETRY
不幸的是,相同的效果会从重试块渗透到周围的 如果有,请重复 batch,如下例所示:
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) 处的整个批次并强制其滚动 回到结尾。
非默认传播呢?
-
在前面的示例中,如果两个事务最终都成功,则 at (3) 可以防止外部 (1) 受到污染。但是,如果 (3) 提交且 (1) 回滚,(3) 保持提交状态,则我们违反了 (1) 的交易合约。如果 (3) 回滚,则 (1) 不一定回滚 (但在实践中可能会这样做,因为 retry 会引发 roll back 异常)。
PROPAGATION_REQUIRES_NEW
TX
TX
TX
TX
TX
TX
TX
TX
-
PROPAGATION_NESTED
at (3) 按照我们在 retry case 中的要求工作(对于 batch with skips):(3) 可以提交,但随后被外部 交易,(1)。如果 (3) 回滚,则 (1) 在实践中回滚。这 选项仅在某些平台上可用,不包括 Hibernate 或 JTA,但它是唯一一个始终有效的。TX
TX
TX
TX
TX
因此,如果 retry 块包含任何数据库,则该模式是最好的
访问。NESTED
特殊情况:具有正交资源的事务
对于没有嵌套数据库的简单情况,默认传播始终是可以的
交易。请考虑以下示例,其中 和 不是
global resources 的 global resources,因此它们的资源是正交的:SESSION
TX
XA
0 | SESSION { 1 | input; 2 | RETRY { 3 | TX { 3.1 | database access; | } | } | }
此处有一个事务型消息 (0),但它不参与其他
transactions 替换为 ,因此它不会在 (3) 时传播
开始。在 (2) 块之外没有数据库访问权限。如果 (3) 失败且
然后最终重试成功,(0) 可以提交(独立于块)。这类似于原版的 “best-effort-one-phase-commit” 场景。这
最糟糕的情况是,当 (2) 成功而 (0) 无法提交时(例如,因为消息系统不可用),则会出现重复的消息。SESSION
PlatformTransactionManager
TX
RETRY
TX
SESSION
TX
RETRY
SESSION
无状态重试无法恢复
在前面显示的典型示例中,无状态重试和有状态重试之间的区别是 重要。实际上,最终是一个事务约束,它强制 distinction 的 Difference 进行区分,而这个约束也清楚地说明了 Distinction 存在的原因。
我们从观察开始,即没有办法跳过失败的项目,并且 成功提交 chunk 的其余部分,除非我们将项目处理包装在 交易。因此,我们将典型的批处理执行计划简化为 遵循:
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; | } | | } | } | | }
前面的示例显示了一个无状态 (3) 和 (5) 路径,该路径 kick
in 的标签表示该块是重复的
而不会重新引发任何异常,但最高达到某个限制。仅当事务 (4) 嵌套了 propagation 时,这才有效。RETRY
RECOVER
stateless
TX
如果内部 (4) 具有默认的传播属性并回滚,它会污染
外层 (1)。事务管理器假定内部事务具有
损坏了事务资源,因此无法再次使用。TX
TX
对嵌套传播的支持非常罕见,因此我们选择不支持 在当前版本的 Spring Batch 中使用无状态重试进行恢复。相同的效果 始终可以通过使用 前面显示的典型模式。