批处理和事务

附录 A:批处理和事务

无需重试的简单批处理

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

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

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

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

简单无状态重试

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

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

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

典型的重复重试模式

最典型的批处理模式是向 chunk,如以下示例所示:spring-doc.cadn.net.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;
|        }
|
|      }
|    }
|
|  }

内层RETRY(4) 块标记为“有状态”。请参阅 描述有状态重试的典型用例。这意味着,如果 重试PROCESS(5)阻塞失败,行为RETRY(4)如下:spring-doc.cadn.net.cn

  1. 抛出异常,回滚事务,TX(2),在块级别,以及 允许将项目重新呈现给输入队列。spring-doc.cadn.net.cn

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

  3. 最终,该项目最后一次重新出现。重试策略不允许另一个 尝试,所以PROCESS(5) 永远不会被执行。在这种情况下,我们遵循RECOVER(6) path,有效地“跳过”已接收和正在处理的项目。spring-doc.cadn.net.cn

请注意,用于RETRY(4)在上述方案中明确表明 输入步骤 (4.1) 是重试的一部分。它还明确指出有两个 处理的备用路径:正常情况,如PROCESS(5) 和 恢复路径,如单独的块中所示RECOVER(6).两条交替路径 是完全不同的。在正常情况下,只会服用一个。spring-doc.cadn.net.cn

在特殊情况下(例如特殊TransactionValidExceptiontype)、重试策略 可能能够确定RECOVER(6) 路径可以在最后一次尝试时采取 后PROCESS(5) 刚刚失败,而不是等待项目重新呈现。 这不是默认行为,因为它需要详细了解 发生在PROCESS(5)块,通常不可用。例如,如果 输出在失败之前包含写访问权限,则异常应为 重新抛出以确保交易完整性。spring-doc.cadn.net.cn

外部的完成策略REPEAT(1)对上述的成功至关重要 计划。如果输出 (5.1) 失败,它可能会抛出异常(通常会抛出异常,因为 描述),在这种情况下,事务,TX(2) 失败,并且异常可能 通过外部批次向上传播REPEAT(1). 我们不希望整批 stop,因为RETRY(4) 如果我们再试一次,可能仍然成功,所以我们添加exception=not critical到外层REPEAT(1).spring-doc.cadn.net.cn

但是请注意,如果TX(2)失败了,我们确实会再试一次,这要归功于外在的 完成策略,接下来在内部处理的项REPEAT(3) 不是保证是刚刚失败的那个。可能是,但这取决于输入的实现 (4.1)。因此,输出 (5.1) 可能会在新项目或旧项目上再次失败。批处理的客户端不应假设每个RETRY(4) 尝试将处理与最后一个失败的项目相同的项目。例如,如果的终止策略REPEAT(1)是10次尝试后失败,10次后失败连续尝试,但不一定在同一项目上。这与整体重试策略。内部RETRY(4) 了解每个项目的历史,并且可以决定是否再次尝试。spring-doc.cadn.net.cn

异步块处理

典型示例中的内部批次或块可以执行同时通过配置外部批次以使用AsyncTaskExecutor. 外部batch 等待所有块完成后再完成。以下示例显示了异步块处理:spring-doc.cadn.net.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;
|        }
|
|      }
|    }
|
|  }

异步项目处理

典型示例中块中的单个项目也可以在原则上并发处理。在这种情况下,事务边界必须移动到单个项目的级别,以便每个事务都在单个线程上,如如以下示例所示:spring-doc.cadn.net.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.cadn.net.cn

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

批量重试和事务管理之间的耦合比我们更紧密理想情况下喜欢。特别是,无状态重试不能用于重试数据库使用不支持 NESTED 传播的事务管理器进行作。spring-doc.cadn.net.cn

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

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

同样,出于同样的原因,内部事务TX(3)、可引起外层 交易TX(1),即使RETRY(2)最终成功。spring-doc.cadn.net.cn

不幸的是,同样的效果会从 retry 块渗透到周围的repeat batch(如果有的话),如以下示例所示:spring-doc.cadn.net.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.cadn.net.cn

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

  • 在前面的示例中,PROPAGATION_REQUIRES_NEWTX(3)防止外层TX(1)如果两笔交易最终都成功,则不会受到污染。但是如果TX(3) 提交和TX(1) 回滚,然后TX(3) 保持承诺,因此我们违反了交易合同TX(1). 如果TX(3)回滚,TX(1) 不一定(但在实践中可能会这样做,因为重试会抛出回滚异常)。spring-doc.cadn.net.cn

  • PROPAGATION_NESTEDTX(3) 在重试情况下按我们的要求工作(对于带有跳过的批次):TX(3) 可以提交,但随后被外部回滚 交易TX(1). 如果TX(3)回滚,TX(1)在实践中倒退。 这 选项仅在某些平台上可用,不包括 Hibernate 或JTA,但它是唯一一个始终有效的平台。spring-doc.cadn.net.cn

因此,NESTED如果重试块包含任何数据库,则模式是最佳的 访问。spring-doc.cadn.net.cn

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

对于没有嵌套数据库的简单情况,默认传播始终是可以的 交易。 考虑以下示例,其中SESSIONTX不是 全球XAresources,因此它们的资源是正交的:spring-doc.cadn.net.cn

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) 无法提交(例如,因为消息系统不可用)。spring-doc.cadn.net.cn

无状态重试无法恢复

在上面的典型示例中,无状态重试和有状态重试之间的区别是 重要。它实际上最终是一个事务约束,迫使 区别,这种约束也清楚地说明了存在区别的原因。spring-doc.cadn.net.cn

我们首先观察到,没有办法跳过失败的项目,并且 成功提交其余的块,除非我们将项目处理包装在 交易。因此,我们将典型的批量执行计划简化为 遵循:spring-doc.cadn.net.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;
|        }
|
|      }
|    }
|
|  }

前面的示例显示了无状态RETRY(3) 与RECOVER(5) 踢球的路径 在最后一次尝试失败后。这stateless标签表示该块被重复 在某个限制内不会重新抛出任何异常。这仅在事务TX(4),具有传播嵌套。spring-doc.cadn.net.cn

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

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