介绍
软件世界是高度不可预测的,其变量范围从网络延迟到第三方服务停机。因此,确保容错性和弹性对于开发健壮的应用程序至关重要。Spring 框架的@Retryable注释提供了一种优雅的方法来自动重试可能因瞬态问题而失败的方法。这篇文章旨在深入@Retryable研究注释的用法,它使基于 Spring 的应用程序能够优雅地处理故障。
Spring的简介@Retryable
在当今的互联世界中,应用程序通常需要与外部服务、数据库和其他资源进行交互。在此过程中,他们会遇到暂时性错误、网络延迟、超时或第三方服务停机,从而导致某些操作的执行不确定。如果您的应用程序的关键代码部分容易受到此类故障情况的影响,您将希望它具有弹性并能够自我恢复,至少对于暂时性问题是这样。这就是 Spring@Retryable注释发挥作用的地方,它为您的应用程序添加了一层容错能力。
重试操作的必要性
想象一个从远程 API 获取数据的服务。在理想情况下,您发出 HTTP 请求,数据就会返回。但在现实世界中,问题就会出现。远程服务器可能负载过重,您自己的服务可能遇到网络延迟,或者可能出现任何其他暂时性问题。如果您的应用程序不能很好地处理这些场景,您最终会导致操作失败、用户愤怒和业务损失。
重试该操作似乎是一个显而易见的解决方案,但在整个应用程序中手动实现它可能会导致代码臃肿且难以维护。这是一个基本的例子:
public class ManualRetryService { public String fetchDataFromRemote() { int attempts = 0; while(attempts < 3) { try { // Make the API call return "Success"; } catch (Exception e) { attempts++; } } throw new MyCustomException("Failed after 3 attempts"); } }
在此示例中,我们使用循环手动实现重试逻辑while,这增加了代码的复杂性。随着您添加更多功能(例如不同的重试间隔、要捕获的不同类型的异常等),管理将变得越来越困难。
如何@Retryable简化流程
Spring 框架通过@Retryable注释简化了这一点。通过此注释,Spring 提供了一种将重试逻辑直接添加到组件中的声明式方法,从而消除了样板代码的需要。这是我们之前的示例的外观@Retryable:
import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Service public class MyService { @Retryable(MyCustomException.class) public String fetchDataFromRemote() { // Make the API call that might fail return "Success"; } }
使用此代码,fetchDataFromRemote如果抛出 .Spring 异常,Spring 将自动重试该方法MyCustomException。该方法变得更加清晰,我们可以使用更高级的重试选项轻松扩展它。
花絮
当您使用 注释方法时@Retryable,Spring 会围绕所注释的方法创建一个代理。这允许框架拦截对该方法的调用并透明地添加重试逻辑。这与 Spring 中使用代理的其他功能类似,例如使用@Transactional.
为什么选择@Retryable手动重试
- 代码清洁度:您的业务逻辑与容错逻辑保持分离。
- 可维护性:更容易扩展或修改重试配置,而无需触及业务代码。
- 可读性:注释使开发人员的意图变得清晰,从而更容易理解方法的预期行为。
通过使用@Retryable注释,您可以向方法添加强大、灵活的重试逻辑,而不会使代码库复杂化。它让您可以专注于业务逻辑,同时框架处理容错,使您的应用程序具有弹性和可维护性。
配置@Retryable
注释@Retryable不是一刀切的解决方案,而是一种高度可定制的功能,可以适应多种场景。它的灵活性是通过一组丰富的配置参数实现的,这些参数使您可以微调重试的管理方式。无论您正在处理简单还是复杂的故障场景,@Retryable注释都能满足您的需求。
指定异常类型
您的方法可能会引发多种类型的异常,但您可能不想对所有异常重试该操作。例如,重试由于 a 而失败的操作NullPointerException可能毫无意义,因为异常可能是由编程错误引起的。另一方面,重试失败的网络操作是有意义的。
使用@Retryable,您可以使用其属性指定应触发重试的异常类型value。您可以通过以下方式告诉 Spring 仅在发生特定异常时重试方法:
@Retryable(value = { MyNetworkException.class, TimeoutException.class }) public String fetchRemoteData () { // 可能失败的网络调用 return "Data" ; }
配置最大尝试次数
默认情况下,@Retryable注释将在放弃之前重试失败的操作最多 3 次。但是,您可以通过设置maxAttempts属性轻松自定义此行为:
@Retryable(value = MyNetworkException.class, maxAttempts = 5) public String fetchRemoteData () { // 可能失败的网络调用 return "Data" ; }
重试之间的延迟
通常,在重试尝试之间引入延迟很有用。这在外部服务可能暂时过载的情况下很有帮助。Spring 允许您通过属性来配置它backoff,该属性接受@Backoff注释。以下示例指定重试尝试之间有两秒的延迟:
@Retryable(value = MyNetworkException.class, backoff = @Backoff(delay = 2000)) public String fetchRemoteData () { // 可能失败的网络调用 return "Data" ; }
指数退避
在某些情况下,您可能需要使用指数退避策略,这会增加每次重试尝试之间的延迟。当您处理需要时间恢复或扩展的服务时,这会很有帮助。注释@Backoff通过其属性支持这一点multiplier:
@Retryable(value = MyNetworkException.class, backoff = @Backoff(delay = 1000, multiplier = 2)) public String fetchRemoteData () { // 可能失败的网络调用 return "Data" ; }
组合多个参数
@Retryable当您开始组合这些属性时,真正的力量就会显现出来。下面是一个为微调重试策略设置多个属性的示例:
@Retryable(value = { MyNetworkException.class, TimeoutException.class }, maxAttempts = 5, backoff = @Backoff(delay = 1000, multiplier = 2)) public String fetchRemoteData () { // 可能失败的网络调用 return "Data" ; }
此示例将重试最多 5 次,仅针对MyNetworkException和TimeoutException,从 1000 毫秒的延迟开始,并将每次后续重试之间的延迟加倍。
使其有条件
在某些情况下,您可能希望根据某些运行时条件或抛出的异常来控制是否动态地继续重试。您可以使用condition该属性来实现此目的,它接受 SpEL 表达式。
@Retryable(value = MyNetworkException.class, condition = "#{#root.args[0] != 'no-retry'}") public String fetchRemoteData (String controlFlag) { // 可能失败的网络调用 return "Data" ; }
在此示例中,如果参数为“no-retry”,则不会继续重试controlFlag。
通过利用这些不同的属性,您可以创建一个高度复杂的重试机制,以满足特定的项目需求,而不会因为容错代码而扰乱您的业务逻辑。
了解参数
该@Retryable注释带有大量参数来自定义重试逻辑。这些参数协调一致,提供了一个开箱即用的强大重试机制。无论您想要具有固定间隔的简单重试,还是需要基于条件重试的指数退避等复杂机制,了解这些参数都将帮助您轻松实现。
value
该value参数指定哪些异常应触发重试。它采用类数组Throwable作为其值。默认情况下,它设置为重试所有扩展的异常Throwable。
@Retryable(value = { MyNetworkException.class, TimeoutException.class }) public Stringexecute ( ) { // 代码 }
include
与 类似value,该include参数允许您指定应触发重试的异常。不同之处在于,include除了已定义的异常之外,还允许指定异常value。
@Retryable(value = MyNetworkException.class, include = TimeoutException.class) public Stringexecute ( ) { // 代码 }
exclude
相反,该exclude参数允许您定义哪些异常不应触发重试。当您想要广泛捕获异常但排除特定异常时,这非常有用。
@Retryable(value = Exception.class, except = IllegalArgumentException.class) public Stringexecute ( ) { // 代码 }
maxAttempts
该maxAttempts参数指示带注释的方法的最大尝试次数。默认值为3。
@Retryable(maxAttempts = 5) public Stringexecute ( ) { // 代码 }
backoff
该backoff参数允许您在重试尝试之间实现延迟。这需要一个@Backoff注释,您可以在其中指定延迟(以毫秒为单位)和一个可选的指数退避乘数。
@Retryable(backoff = @Backoff(delay = 2000, multiplier = 2)) public Stringexecute ( ) { // 代码 }
condition
该condition参数允许您指定计算结果为布尔值的 SpEL(Spring 表达式语言)表达式。仅当该表达式的计算结果为 时,才会激活重试逻辑true。
@Retryable(condition = "#{#arg > 100}") public String execute ( int arg) { // 代码 }
stateful
该stateful参数指定重试应该是有状态的还是无状态的。在有状态重试中,会记住第一次失败尝试的状态,并据此进行后续重试。另一方面,无状态重试是相互独立的。
@Retryable ( stateful = true) public Stringexecute () { // 代码}
listeners
该listeners参数允许您指定将在每次重试尝试时收到通知的 bean。该 bean 必须实现该RetryListener接口。这对于日志记录、指标或其他副作用很有用。
@Retryable(listeners = "myRetryListenerBean") public Stringexecute ( ) { // 代码 }
把它们放在一起
当这些参数结合使用来创建复杂的重试机制时,真正的魔力就会发生。这是一个例子:
@Retryable(value = { MyNetworkException.class, TimeoutException.class }, maxAttempts = 5, backoff = @Backoff(delay = 2000, multiplier = 2), condition = "#{#arg != 'no-retry'}") public Stringexecute (String arg) { //代码}
MyNetworkException仅当抛出or时,此示例才会重试该方法最多 5 次TimeoutException。重试将以 2000 毫秒的初始延迟进行,每次尝试后延迟加倍,并且仅当参数arg不是时才会继续'no-retry'。
通过深入了解这些参数及其相互作用,您就可以@Retryable以最有效的方式使用这些参数,从而确保您的应用程序尽可能具有弹性和容错能力。
结合@Retryable @Recover
使用@Retryable注释时,必须考虑如果所有重试都失败会发生什么情况。虽然重试可以增加操作成功的机会,但不能保证成功。这就是@Recover注释发挥作用的地方。
@Recover的作用
该@Recover注释允许您定义一个后备方法,当配置的所有重试尝试都用完时,将调用该方法@Retryable。回退方法旨在执行替代逻辑,例如发送错误消息、尝试连接到备份服务或更新应用程序状态以反映故障。
下面用一个简单的例子来说明它的使用:
import org.springframework.retry.annotation.Recover; import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Service public class MyService { @Retryable(MyNetworkException.class) public String fetchDataFromRemote() { // Network call that might fail return "Data"; } @Recover public String recover(MyNetworkException e) { // Fallback logic return "Default Data"; } }
在此示例中,如果该fetchDataFromRemote方法抛出 aMyNetworkException并耗尽所有重试尝试,recover则将调用该方法,并返回“默认数据”作为后备。
匹配异常类型
该@Recover方法的参数列表必须与该@Retryable方法的参数列表匹配,但要为要从中恢复的异常类型添加一个附加的第一个参数。
例如,如果该@Retryable方法采用两个这样的参数:
@Retryable(MyNetworkException.class) public String fetchData (String param1, int param2) { // 网络调用 }
然后,@Recover方法签名可能如下所示:
@Recover public String recovery (MyNetworkException e, String param1, int param2) { // 回退逻辑 }
多种恢复路径
您可以@Recover为不同类型的异常定义多种方法。这样,您可以根据导致所有重试失败的异常类型执行不同的恢复逻辑。您可以这样设置:
@Retryable(value = { MyNetworkException.class, TimeoutException.class }) public String fetchDataFromRemote () { // 网络调用 } @Recover public String recovery (MyNetworkException e) { return "Default Data for MyNetworkException" ; } @Recover public String recovery (TimeoutException e) { return "TimeoutException 的默认数据" ; }
在此示例中,有两种@Recover方法:一种是 for MyNetworkException,另一种是 for TimeoutException。@Recover根据导致重试次数耗尽的异常,将调用适当的方法。
使其有条件
与 类似@Retryable,您还可以@Recover使用带有 SpEL(Spring 表达式语言)表达式的参数向方法添加条件condition。这使您可以根据动态条件甚至微调您的后备行为。
@Recover public String recovery (MyNetworkException e, String param1) { if ( "special_case" .equals(param1)) { return "特殊恢复逻辑" ; } return "通用恢复逻辑" ; }
何时使用@Recover
虽然@Retryable可以帮助从暂时性故障中恢复,@Recover但当您必须处理更持久的问题或在所有重试尝试失败后想要执行“B 计划”时,它就会发挥作用。
通过与 结合使用@Retryable,@Recover您可以构建一个强大的、自我恢复的系统,能够处理暂时性和更持久的问题,确保更高水平的容错能力并改善整体用户体验。
用例
远程服务调用
当您的应用程序依赖于可能暂时不可用或面临间歇性问题的远程服务时,使用@Retryable可以增加成功完成操作的可能性。
@Retryable(MyNetworkException.class) public String fetchFromRemoteService () { // 对外部 API 的 HTTP 请求 return "Data" ; }
分布式系统
在微服务或分布式架构中,网络故障或服务临时不可用是很常见的。@Retryable可以确保您的系统能够抵御此类故障。
@Retryable(TimeoutException.class) public void sendMessageToQueue (String message) { // 发送消息到消息队列 }
数据库
有时,数据库操作可能会由于死锁或临时连接问题而失败。重试事务通常可以解决这些问题。
@Retryable(DatabaseException.class) public void updateDatabaseRecord () { // 更新数据库记录 }
文件操作
文件操作可能会由于缺乏权限或磁盘空间等各种原因而失败。解决导致失败的具体问题后,重试可能会有效。
@Retryable(IOException.class) public void writeFile () { // 写入文件 }
复杂条件重试
您可以使用该condition参数根据运行时条件实现复杂的重试逻辑,使其非常灵活。
@Retryable(value = CustomException.class, condition = "#{#someArg > 100}") public void complexConditionMethod ( int someArg) { // 做某事 }
局限性
性能开销
每次重试都会消耗资源,无论是 CPU 周期、内存,甚至是网络带宽。过多的重试可能会导致性能瓶颈。
并不适合所有错误
并非所有类型的错误都可以重试。例如,由于“找不到文件”异常而重试失败的操作可能会导致重复失败。
级联故障
微服务架构中的重试次数过多可能会导致级联故障,其中一项失败的服务也会导致其他服务失败。
有状态系统的复杂性
在维护状态的系统中,更改状态的失败操作可能会使重试变得复杂。
错误处理
使用@Recover方法可能会导致错误处理逻辑分散,这在较大的代码库中可能难以管理。
结论
Spring 中的和注释提供了一种优雅的声明式方法来向应用程序添加重@Retryable试@Recover逻辑和容错能力。虽然它们提供了丰富的可定制选项,但明智地使用它们非常重要,同时牢记它们的用例和限制。通过了解其功能的深度并明智地应用它们,您可以显着增强应用程序的弹性和稳健性。