批处理和事务

附录 A:批处理和事务

简单批处理,无需重试

请考虑以下没有重试的嵌套批处理的简单示例。它显示了一个 批处理的常见场景:处理输入源直到耗尽,并且 它会在处理的 “chunk” 结束时定期提交。spring-doc.cn

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

输入操作 (3.1) 可以是基于消息的接收(例如来自 JMS)或 基于文件的读取,但要恢复并继续处理,并有机会完成 整个 job,它必须是事务性的。这同样适用于 3.2 中的操作。它必须 可以是 transactional 或 idempotent。spring-doc.cn

如果 (3) 处的块由于 3.2 处的数据库异常而失败,则 (2) 必须回滚整个 chunk。REPEATTXspring-doc.cn

简单无状态重试

对非事务性操作(如 调用 Web 服务或其他远程资源,如下例所示:spring-doc.cn

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

这实际上是重试最有用的应用程序之一,因为远程调用是 比数据库更新更有可能失败并且可重试。只要遥控器 Access (2.1) 最终成功,事务 (0) 提交。如果远程 Access (2.1) 最终失败,事务 (0) 保证滚动 返回。TXTXspring-doc.cn

典型的重复-重试模式

最典型的批处理模式是将重试添加到 块,如下例所示:spring-doc.cn

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) 的行为如下:RETRYPROCESSRETRYspring-doc.cn

  1. 抛出异常,回滚事务 (2),在 chunk 级别,以及 允许将项目重新呈现给 Input 队列。TXspring-doc.cn

  2. 当项目重新出现时,可能会重试,具体取决于现有的重试策略,并且 再次执行 (5)。第二次和后续尝试可能会再次失败,并且 重新引发异常。PROCESSspring-doc.cn

  3. 最终,该项目最后一次重新出现。重试策略不允许另一个 attempt,因此 (5) 永远不会执行。在这种情况下,我们遵循 (6) 路径,有效地 “跳过” 已接收并正在处理的项目。PROCESSRECOVERspring-doc.cn

请注意,计划中用于 (4) 的表示法明确表明 输入步骤 (4.1) 是重试的一部分。它还清楚地表明有两个 处理的替代路径:正常情况,如 (5) 所示,并且 recovery path,如在单独的块中用 (6) 表示的那样。两条备用路径 是完全不同的。在正常情况下,只服用过一次。RETRYPROCESSRECOVERspring-doc.cn

在特殊情况(如特殊类型)中,重试策略 可能能够确定 (6) 路径可以在最后一次尝试时采用 after (5) 刚刚失败,而不是等待项目重新呈现。 这不是默认行为,因为它需要详细了解 发生在 (5) 块内,这通常不可用。例如,如果 失败前的输出包括 write access,异常应该是 rethrown 以确保事务完整性。TranscationValidExceptionRECOVERPROCESSPROCESSspring-doc.cn

外部 (1) 中的 completion 策略对于 计划。如果输出 (5.1) 失败,它可能会抛出一个异常(通常如此,如 描述),在这种情况下,事务 (2) 会失败,并且异常可能会 通过外部批处理向上传播 (1)。我们不希望整个批次 stop,因为如果我们再试一次,(4) 可能仍然会成功,所以我们添加到外部的 (1) 中。REPEATTXREPEATRETRYexception=not criticalREPEATspring-doc.cn

但是请注意,如果 (2) 失败并且我们再次尝试,则凭借外部 completion 策略中,在内部 (3) 中下一个处理的 Item 不是 保证是刚刚失败的那个。可能是,但这取决于 input 的实现 (4.1)。因此,输出 (5.1) 可能会在 新项目或旧项目。批处理的客户端不应假定每个 (4) 尝试将处理与上一个失败的项目相同的项目。例如,如果 (1) 的终止策略是在 10 次尝试后失败,在 10 次尝试后失败 连续尝试,但不一定是针对同一项。这与 总体重试策略。内部 (4) 知道每个项目的历史记录,并且 可以决定是否再次尝试。TXREPEATRETRYREPEATRETRYspring-doc.cn

异步块处理

典型示例中的内部 batches 或 chunk 可以执行 同时,将外部批处理配置为使用 .外部 batch 等待所有 chunk 完成后再完成。以下示例显示了 异步块处理:AsyncTaskExecutorspring-doc.cn

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 中的单个项目也可以在 原则,同时处理。在这种情况下,事务边界必须移动 添加到单个项的级别,以便每个事务都位于单个线程上,因为 以下示例显示:spring-doc.cn

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) 的成本。spring-doc.cn

批处理和事务传播之间的交互

批处理重试和事务管理之间的耦合比我们更紧密 理想情况下是这样的。特别是,无状态重试不能用于重试数据库 操作。spring-doc.cn

以下示例使用不重复的重试:spring-doc.cn

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

同样,出于同样的原因,内部事务 (3) 可以导致外部 transaction (1) 失败,即使 (2) 最终成功。TXTXRETRYspring-doc.cn

不幸的是,相同的效果会从重试块渗透到周围的 如果有,请重复 batch,如下例所示:spring-doc.cn

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) 处的整个批次并强制其滚动 回到结尾。spring-doc.cn

非默认传播呢?spring-doc.cn

  • 在前面的示例中,如果两个事务最终都成功,则 at (3) 可以防止外部 (1) 受到污染。但是,如果 (3) 提交且 (1) 回滚,(3) 保持提交状态,则我们违反了 (1) 的交易合约。如果 (3) 回滚,则 (1) 不一定回滚 (但在实践中可能会这样做,因为 retry 会引发 roll back 异常)。PROPAGATION_REQUIRES_NEWTXTXTXTXTXTXTXTXspring-doc.cn

  • PROPAGATION_NESTEDat (3) 按照我们在 retry case 中的要求工作(对于 batch with skips):(3) 可以提交,但随后被外部 交易,(1)。如果 (3) 回滚,则 (1) 在实践中回滚。这 选项仅在某些平台上可用,不包括 Hibernate 或 JTA,但它是唯一一个始终有效的。TXTXTXTXTXspring-doc.cn

因此,如果 retry 块包含任何数据库,则该模式是最好的 访问。NESTEDspring-doc.cn

特殊情况:具有正交资源的事务

对于没有嵌套数据库的简单情况,默认传播始终是可以的 交易。请考虑以下示例,其中 和 不是 global resources 的 global resources,因此它们的资源是正交的:SESSIONTXXAspring-doc.cn

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) 无法提交时(例如,因为消息系统不可用),则会出现重复的消息。SESSIONPlatformTransactionManagerTXRETRYTXSESSIONTXRETRYSESSIONspring-doc.cn

无状态重试无法恢复

在前面显示的典型示例中,无状态重试和有状态重试之间的区别是 重要。实际上,最终是一个事务约束,它强制 distinction 的 Difference 进行区分,而这个约束也清楚地说明了 Distinction 存在的原因。spring-doc.cn

我们从观察开始,即没有办法跳过失败的项目,并且 成功提交 chunk 的其余部分,除非我们将项目处理包装在 交易。因此,我们将典型的批处理执行计划简化为 遵循:spring-doc.cn

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 时,这才有效。RETRYRECOVERstatelessTXspring-doc.cn

如果内部 (4) 具有默认的传播属性并回滚,它会污染 外层 (1)。事务管理器假定内部事务具有 损坏了事务资源,因此无法再次使用。TXTXspring-doc.cn

对嵌套传播的支持非常罕见,因此我们选择不支持 在当前版本的 Spring Batch 中使用无状态重试进行恢复。相同的效果 始终可以通过使用 前面显示的典型模式。spring-doc.cn