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