Appendix A: 批处理和事务
本站( springdoc.cn )中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。 |
无需重试的简单批处理
下面是一个没有重试的嵌套批处理的简单示例。它展示了批处理的一种常见情况: 输入源一直被处理到耗尽,并在处理 "块" 结束时定期提交。
1 | REPEAT(until=exhausted) { | 2 | TX { 3 | REPEAT(size=5) { 3.1 | input; 3.2 | output; | } | } | | }
输入操作 (3.1) 可以是基于消息的接收(如从 JMS),也可以是基于文件的读取,但要恢复和继续处理并有机会完成整个作业,它必须是事务性的。这同样适用于 3.2 中的操作。它必须是事务性的或幂等的。
如果 REPEAT
(3) 处的数据块因 3.2 处的数据库异常而失败,则 TX
(2) 必须回滚整个数据块。
简单的无状态重试
对于非事务性操作,如调用 web 服务或其他远程资源,重试也很有用,如下例所示:
0 | TX { 1 | input; 1.1 | output; 2 | RETRY { 2.1 | remote access; | } | }
这实际上是重试最有用的应用之一,因为远程调用比数据库更新更容易失败和重试。只要远程访问 (2.1) 最终成功,事务 TX
(0) 就会提交。如果远程访问 (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) 块标记为 "有状态"。有关有状态重试的描述,请参阅 典型用例。这意味着,如果重试 PROCESS
(5) 块失败,RETRY
(4) 的行为如下:
-
抛出异常,在大块级别回滚
TX
(2) 事务,允许项目(item)重新提交到输入队列。 -
当项目再次出现时,可能会根据重试策略再次执行
PROCESS
(5)。第二次及以后的尝试可能会再次失败,并重新抛出异常。 -
最后,该项目最后一次重新出现。重试策略不允许再次尝试,因此
PROCESS
(5) 永远不会被执行。在这种情况下,我们将遵循RECOVER
(6) 路径,有效地 "跳过" 已收到并正在处理的项目。
请注意,计划中用于 RETRY
(4) 的符号明确显示输入 step (4.1) 是重试的一部分。它还清楚地表明有两条交替处理路径:一条是正常情况,用 PROCESS
(5) 表示;另一条是恢复路径,用 RECOVER
(6) 表示。这两条备用路径完全不同。在正常情况下,只走一条。
在特殊情况下(如特殊的 TranscationValidException
类型),重试策略可能会确定在 PROCESS
(5) 失败后的最后一次尝试中可以采用 RECOVER
(6) 路径,而不是等待项目重新提交。这并不是默认行为,因为它需要详细了解 PROCESS
(5) 块内部发生了什么,而这通常是不可能的。例如,如果输出在失败前包含写访问,则应重新抛出异常,以确保事务完整性。
外层 REPEAT
(1) 中的完成策略对计划的成功至关重要。如果输出 (5.1) 失败,它可能会抛出异常(如上所述,它通常会抛出异常),在这种情况下,事务 TX
(2) 会失败,异常可能会通过外层批处理 REPEAT
(1) 向上传播。我们不希望整个批处理停止,因为如果我们再试一次,RETRY
(4) 可能仍然会成功,所以我们在外部 REPEAT
(1) 中添加了 exception=not critical
。
但是,请注意,如果 TX
(2) 失败,我们再次尝试,根据外部完成策略,在内部 REPEAT
(3) 中下一步处理的项目并不能保证就是刚刚失败的那个。有可能是,但这取决于输入 (4.1) 的实现。因此,输出 (5.1) 可能会在新项目或旧项目上再次失败。批处理的客户端不应假定每次 RETRY
(4) 都会处理与上次失败相同的项目。例如,如果 REPEAT
(1) 的终止策略是尝试 10 次后失败,那么它就会在连续尝试 10 次后失败,但不一定是在同一个项目上。这与整个重试策略是一致的。内部 RETRY
(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; | } | | } | } | | }
异步项目处理
原则上,典型示例 中以块为单位的单个项目也可以同时处理。在这种情况下,事务边界必须移动到单个项目的级别,以便每个事务都在一个线程上,如下例所示:
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)时,该计划才会有用。
批处理与事务传播之间的相互作用
批量重试和事务管理之间的耦合比我们理想的要紧密。特别是,无状态重试不能用于重试不支持 NESTED
传播的事务管理器的数据库操作。
下面的示例使用了不重复的重试:
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) 的事务契约。如果TX
(3) 回滚,TX
(1) 不一定回滚(但实际上可能回滚,因为重试会抛出回滚异常)。 -
TX
(3) 中的PROPAGATION_NESTED
在重试情况下(以及在有跳转的批次中)可以按照我们的要求运行:TX
(3) 可以提交,但随后会被外部事务TX
(1) 回滚。如果TX
(3) 回滚,TX
(1) 实际上也会回滚。该选项仅在某些平台上可用,不包括 Hibernate 或 JTA,但它是唯一能持续工作的选项。
因此,如果重试块包含任何数据库访问,最好使用 NESTED
模式。
特例:正交资源和事务
在没有嵌套数据库事务的简单情况下,默认传播总是没有问题的。请看下面的示例,其中 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) 无法提交(例如,由于消息系统不可用)时,会出现重复消息。
无状态重试无法恢复
在前面的典型示例中,无状态重试和有状态重试之间的区别非常重要。实际上,这种区别最终是由事务约束造成的,而这种约束也使区别的存在显而易见。
我们首先会发现,除非将项目处理打包到事务中,否则无法跳过失败的项目并成功提交该块的其余部分。因此,我们将典型的批处理执行计划简化如下:
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) 路径。stateless
标签的意思是,在某个限制范围内,重复执行该代码块时不会再抛出任何异常。只有当事务 TX
(4) 嵌套了传播时,这种方法才有效。
如果内部 TX
(4) 具有默认传播属性并回滚,则会污染外部 TX
(1)。事务管理器会认为内部事务已损坏事务资源,因此无法再次使用。
对嵌套传播的支持非常罕见,因此我们选择在当前版本的 Spring Batch 中不支持无状态重试恢复。使用前面所示的典型模式,总能达到相同的效果(代价是重复更多的处理)。