Retry
重试一个失败的操作有助于保证系统工作流程稳固性和正确性。实际工作中,一些间歇性故障触发的系统错误往往是暂时的。例如,对 Web 服务的远程调用会因为网络波动或者数据库死锁而失败。
RetryTemplate
重试功能在 Spring Batch 2.2.0. 版本的时候被移除了,现在ta是 Spring Retry 库的一部分。
为了自动化重试机制,Spring Batch 有一种重试操作策略。改策略的接口代码如下:
1 | public interface RetryOperations { |
RetryCallback 是一个简单的接口,ta允许你添加一些业务逻辑去重试。具体代码如下:
1 | public interface RetryCallback<T, E extends Throwable> { |
RetryCallback 运行过程中如果发生错误(然后抛出了一个 Exception),ta会一直重试到成功或者执行结束。
RetryOperations 接口有多个重载的 execute 方法,用来处理各种不同的事件,当所有的重试都用完的时候还可以改善和处理重试状态。这也允许了客户端和实现之间调用过程中保存信息(我们将在后面更详细地介绍这一点)。
RetryOperations 最简单最通用的一个实现就是 RetryTemplate,ta可以这样使用:
1 | RetryTemplate template = new RetryTemplate(); |
在上面的例子里,我们创建了一个 Web 服务回调,并且返回一个结果给调用方。如果这个调用失败了,ta就会重试,直到超时。
RetryContext
RetryCallback 的方法参数是 RetryContext。许多回调会忽略上下文,但如有必要,在迭代期间,ta可以作为属性包来存储数据。
如果在同一线程中正在进行嵌套重试,则 RetryContext 具有父上下文。父上下文偶尔会用来存储数据,并在 execute
回调之间共享。
RecoveryCallback
当重试用完时,RetryOperations 可以将控制权传递给不同的回调,称为 RecoveryCallback。要使用此功能,客户端需要将RetryCallback 和 RecoveryCallback 一起传递给相同的方法,如下所示:
1 | Foo foo = template.execute(new RetryCallback<Foo>() { |
如果template终止之前业务逻辑都没有执行成功,客户端可以通过 RecoveryCallback 执行备用流程。
Stateless Retry
最简单情况下,重试只是一个while循环,RetryTemplate 会一直尝试知道执行成功或者失败。RetryContext 包含一些决定重试或者终止的状态。这个状态是保存在栈中的,不需要全局保存,我们称之为无状态重试(Stateless Retry)。无状态重试和有状态重试的区别在于 RetryPolicy 的实现上(RetryTemplate可以处理两者)。再无状态重试中,重试回调会在发生错误的线程中执行。
Stateful Retry
在失败导致事务资源失败的情况下,需要注意一些特别的事项。这不适用于简单远程调用,因为ta没有事务资源(通常是没有的),但有时候ta在数据库更新时又是适用的,尤其是使用Hibernate的时候。在这种情况下,只有立刻抛出异常表示执行失败才是合理的,然后我们就可以回滚事务,并且开启一个新的有效的事务。
在涉及事务的例子中,无状态重试并不适用,因为重新抛出和回滚必然涉及离开 RetryOperations.execute() 方法并可能丢失堆栈上的上下文。为了避免丢失它,我们必须引入一种存储策略将它从堆栈中取出并放入堆存储中。为此,Spring Batch 提供了一种名为 RetryContextCache 的存储策略,我们可以把ta注入到 RetryTemplate。RetryContextCache 的默认实现是使用一个简单的 Map 保存到内存中。在集群环境中使用多个进程的高级用法也可能考虑使用某种集群缓存来实现 RetryContextCache(但是,即使在集群环境中,这也可能是矫枉过正)。
RetryOperations 有一部分职责是当它们返回新的执行时(通常包含在新的事务中)识别失败的操作。 为了促进这一点,Spring Batch 提供了 RetryState。 这需要与 RetryOperations 接口中的特殊执行方法结合使用。
识别失败操作的方式是通过多次调用重试来识别状态。 为了标识状态,用户可以提供一个 RetryState 对象,该对象负责返回标识项目的唯一键。 该标识符用作 RetryContextCache 接口中的键。
在 RetryState 返回的键中实现 Object.equals() 和 Object.hashCode() 时要非常小心。 最好的建议是使用业务密钥来识别项目。 对于 JMS 消息,可以使用消息 ID。
当重试用完时,还可以选择以不同的方式处理失败的项目,而不是调用 RetryCallback(现在假定很可能会失败)。 就像在无状态情况下一样,此选项由 RecoveryCallback 提供,可以通过将其传递给 RetryOperations 的 execute 方法来提供。
重试与否的决定权委托给了一个固定的 RetryPolicy,所以日常涉及的限制和超时设置都可以在这里注入(本章稍后描述)。
Retry Policies
在 RetryTemplate 中,execute 方法是重试还是返回失败取决于 RetryPolicy,RetryPolicy 它也是 RetryContext 的工厂。RetryTemplate 负责使用当前 policy(策略) 创建 RetryContext 并在每次重试时将其传递给 RetryCallback。回调失败后,RetryTemplate 必须调用 RetryPolicy 以要求它更新其状态(存储在 RetryContext 中),然后询问 policy 是否可以进行另一次重试。如果不能进行另一次尝试(例如触发限制条件或检测到超时),则策略还负责处理这个耗尽状态。简单的实现会抛出 RetryExhaustedException 异常,这会导致任何封闭的事务被回滚。更复杂的实现可能会尝试采取一些恢复操作,在这种情况下,事务可以保持完整。
失败本质上是可重试或不可重试的。如果总是从业务逻辑中抛出相同的异常,那么重试是没有用的。所以不要什么类型的异常都用重试处理。尽量只关注那些您希望去重试的异常。积极地重试虽然不会损害程序,但是会造成浪费。如果已经注定会失败,你还花时间去重试,这就是一场灾难了。
Spring Batch为无状态的 RetryPolicy 提供了一些简单的通用实现,例如SimpleretryPolicy和TimeoutretryPolicy(在前面使用过的示例)。
SimpleRetryPolicy 允许对指定的异常类型列表中的任何一种进行重试,重试的次数不会超过设定值。它也有一个 “致命 “异常的列表,这些异常不会被重试,该“致命”列表会覆盖可重试的列表,所以可以用它来对重试行为进行更精细的控制,如下所示:
1 | SimpleRetryPolicy policy = new SimpleRetryPolicy(); |
还有一个更灵活的实现,叫做 ExceptionClassifierRetryPolicy,它让用户通过 ExceptionClassifier 为一组任意的异常类型配置不同的重试行为。该策略通过调用分类器将异常转换为一个委托的RetryPolicy来工作。例如,一种异常类型可以通过映射到不同的策略,在失败前被重试的更多次。
用户可能需要实现他们自己的重试策略,以做出更多的自定义的决策。例如,当一个众所周知的特定问题的异常发生的时候,如果这个异常可以重试也可以不重试,那么这时候采用自定义重试策略就很有意义了。
Backoff Policies
失败后重试时,有一个很有用的技巧就是等待一下。因为失败通常由于一些只有等待才能解决的问题引起的。如果 RetryCallback 失败,RetryTemplate 可以根据 BackoffPolicy 暂停执行。
下面是 BackOffPolicy 接口的代码:
1 | public interface BackoffPolicy { |
BackoffPolicy可以选择用任何方式实现 backOff,Spring Batch开箱即用的策略都使用Object.wait()。一个常见的用例是用一个指数级增长的等待期来回退,以避免两次重试在进入到锁步骤时都失败(这是从以太网中吸取的教训)。为此,Spring Batch提供了ExponentialBackoffPolicy。
Listeners
通常,能够接收多个不同重试都涉及的交互点的额外回调是非常有用的。为此,Spring Batch提供了RetryListener接口。RetryTemplate让用户注册RetryListeners,在迭代过程中,如果有RetryContext和Throwable,它们会被赋予回调功能。
下面是 RetryListener 接口的代码:
1 | public interface RetryListener { |
打开或者关闭回调,取决于整个重试,或者取决于错误地请求了单个的RetryCallback调用。如果发生了错误,close方法也可能会收到一个 Throwable,它是RetryCallback抛出的最后一个错误。
请记住,当有一个以上的监听器存在于一个 list 中时,ta们就会有顺序关系。在这种情况下,open 方法的调用顺序相同,而onError和close方法的调用顺序相反。
Declarative Retry
有时候,有些业务流程执行的时候是需要每次都重试的。典型例子就是远程服务调用。为此,SpringBatch提供了一个AOP拦截器,用于将方法调用包装到RetryOperations实现中。RetryOperationsInterceptor 执行拦截方法,然后在失败时用 RepeatTemplate 提供的 RetryPolicy 来重试。
以下示例显示了一个声明性重试,它使用java配置的方式来重试对 remoteCall 方法的服务调用(有关如何配置AOP拦截器的更多详细信息,请参阅Spring用户指南):
1 |
|
上面的例子在拦截器中使用了一个默认的RetryTemplate。要改变策略或监听器,可以把RetryTemplate的实例注入拦截器中。
Version 4.3.4
Last updated 2021-11-17 04:56:00 UTC