Spring Retry

小小小幸运  金牌会员 | 2024-1-21 15:58:36 | 来自手机 | 显示全部楼层 | 阅读模式
打印 上一主题 下一主题

主题 729|帖子 729|积分 2187

工作中,经常遇到需要重试的场景,最简单的方式可以用try...catch...加while循环来实现。那么,有没有统一的、优雅一点儿的处理方式呢?有的,Spring Retry就可以帮我们搞定重试问题。
关于重试,我们可以关注以下以下几个方面:

  • 什么情况下去触发重试机制
  • 重试多少次,重试的时间间隔
  • 是否可以对重试过程进行监视
接下来,带着这些思考,一起看下Spring Retry是如何解决这些问题的
首先,引入依赖。
  1. <dependency>
  2.     <groupId>org.springframework.retry</groupId>
  3.     <artifactId>spring-retry</artifactId>
  4. </dependency>
  5. <dependency>
  6.     <groupId>org.springframework.boot</groupId>
  7.     <artifactId>spring-boot-starter-aop</artifactId>
  8. </dependency>
复制代码
有两种使用方式:命令式和声明式
1. 命令式
  1. RetryTemplate template = RetryTemplate.builder()
  2.         .maxAttempts(3)
  3.         .fixedBackoff(1000)
  4.         .retryOn(RemoteAccessException.class)
  5.         .build();
  6. template.execute(ctx -> {
  7.     // ... do something
  8. });
复制代码
命令式主要是利用RetryTemplate。RetryTemplate 实现了 RetryOperations 接口。
  1. RetryTemplate template = new RetryTemplate();
  2. TimeoutRetryPolicy policy = new TimeoutRetryPolicy();
  3. policy.setTimeout(30000L);  //  30秒内可以重试,超过30秒不再重试
  4. template.setRetryPolicy(policy);
  5. MyObject result = template.execute(new RetryCallback<MyObject, Exception>() {
  6.     public MyObject doWithRetry(RetryContext context) {
  7.         // Do stuff that might fail, e.g. webservice operation
  8.         return result;
  9.     }
  10. });
复制代码
RetryTemplate 也支持流式配置
  1. //  最大重试10次,第一次间隔100ms,第二次200ms,第三次400ms,以此类推,最大间隔10000ms
  2. RetryTemplate.builder()
  3.       .maxAttempts(10)
  4.       .exponentialBackoff(100, 2, 10000)
  5.       .retryOn(IOException.class)
  6.       .traversingCauses()
  7.       .build();
  8. //  3秒内可以一直重试,每次间隔10毫秒,3秒以后就不再重试了
  9. RetryTemplate.builder()
  10.       .fixedBackoff(10)
  11.       .withinMillis(3000)
  12.       .build();
  13. //  无限重试,间隔最小1秒,最大3秒
  14. RetryTemplate.builder()
  15.       .infiniteRetry()
  16.       .retryOn(IOException.class)
  17.       .uniformRandomBackoff(1000, 3000)
  18.       .build();
复制代码
当重试耗尽时,RetryOperations可以将控制传递给另一个回调:RecoveryCallback
  1. template.execute(new RetryCallback<Object, Throwable>() {
  2.     @Override
  3.     public Object doWithRetry(RetryContext context) throws Throwable {
  4.         // 业务逻辑
  5.         return null;
  6.     }
  7. }, new RecoveryCallback<Object>() {
  8.     @Override
  9.     public Object recover(RetryContext context) throws Exception {
  10.         //  恢复逻辑
  11.         return null;
  12.     }
  13. });
复制代码
如果重试次数耗尽时,业务逻辑还没有执行成功,那么执行恢复逻辑来进行兜底处理(兜底方案)
无状态的重试
在最简单的情况下,重试只是一个while循环:RetryTemplate可以一直尝试,直到成功或失败。RetryContext包含一些状态,用于确定是重试还是中止。然而,这个状态是在堆栈上的,不需要在全局的任何地方存储它。因此,我们称之为“无状态重试”。无状态重试和有状态重试之间的区别包含在RetryPolicy的实现中。在无状态重试中,回调总是在重试失败时的同一个线程中执行。
有状态的重试
如果故障导致事务性资源失效,则需要考虑一些特殊问题。这并不适用于简单的远程调用,因为(通常)没有事务性资源,但它有时适用于数据库更新,特别是在使用Hibernate时。在这种情况下,只有重新抛出立即调用失败的异常才有意义,这样事务才能回滚,我们才能开始一个新的(有效的)事务。在这些情况下,无状态重试还不够好,因为重新抛出和回滚必然涉及离开RetryOperations.execute()方法,并且可能丢失堆栈上的上下文。为了避免丢失上下文,我们必须引入一种存储策略,将其从堆栈中取出,并(至少)将其放入堆存储中。为此,Spring Retry提供了一个名为RetryContextCache的存储策略,您可以将其注入到RetryTemplate中。RetryContextCache的默认实现是在内存中,使用一个简单的Map。它具有严格强制的最大容量,以避免内存泄漏,但它没有任何高级缓存特性(例如生存时间)。如果需要,你应该考虑注入具有这些特性的Map。
重试策略
在RetryTemplate中,由RetryPolicy决定是重试还是失败。RetryTemplate负责使用当前策略创建RetryContext,并在每次重试时将其传递给RetryCallback。回调失败后,RetryTemplate必须调用RetryPolicy,要求它更新自己的状态(存储在RetryContext中)。然后询问政策是否可以再尝试一次。如果不能进行另一次重试(例如,因为已达到限制或检测到超时),策略还负责标识耗尽状态——但不负责处理异常。当没有恢复可用时,RetryTemplate抛出原始异常,但有状态情况除外。在这种情况下,它会抛出RetryExhaustedException。还可以在RetryTemplate中设置一个标志,让它无条件地抛出回调(即用户代码)中的原始异常。
  1. // Set the max attempts including the initial attempt before retrying
  2. // and retry on all exceptions (this is the default):
  3. SimpleRetryPolicy policy = new SimpleRetryPolicy(5, Collections.singletonMap(Exception.class, true));
  4. // Use the policy...
  5. RetryTemplate template = new RetryTemplate();
  6. template.setRetryPolicy(policy);
  7. template.execute(new RetryCallback<MyObject, Exception>() {
  8.     public MyObject doWithRetry(RetryContext context) {
  9.         // business logic here
  10.     }
  11. });
复制代码
监听器 
Spring Retry提供了RetryListener接口。RetryTemplate允许您注册RetryListener实例。
  1. template.registerListener(new RetryListener() {
  2.     @Override
  3.     public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
  4.         return false;
  5.     }
  6.     @Override
  7.     public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
  8.     }
  9.     @Override
  10.     public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
  11.     }
  12. });
复制代码
反射方法调用的监听器
  1. template.registerListener(new MethodInvocationRetryListenerSupport() {
  2.     @Override
  3.     protected <T, E extends Throwable> void doClose(RetryContext context, MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
  4.         super.doClose(context, callback, throwable);
  5.     }
  6.     @Override
  7.     protected <T, E extends Throwable> void doOnError(RetryContext context, MethodInvocationRetryCallback<T, E> callback, Throwable throwable) {
  8.         super.doOnError(context, callback, throwable);
  9.     }
  10.     @Override
  11.     protected <T, E extends Throwable> boolean doOpen(RetryContext context, MethodInvocationRetryCallback<T, E> callback) {
  12.         return super.doOpen(context, callback);
  13.     }
  14. });
复制代码


2. 声明式
  1. @EnableRetry
  2. @SpringBootApplication
  3. public class Application {
  4.     public static void main(String[] args) {
  5.         SpringApplication.run(Application.class, args);
  6.     }
  7. }
  8. @Service
  9. class Service {
  10.     @Retryable(RemoteAccessException.class)
  11.     public void service() {
  12.         // ... do something
  13.     }
  14.     @Recover
  15.     public void recover(RemoteAccessException e) {
  16.        // ... panic
  17.     }
  18. }
复制代码
可以将@EnableRetry注释添加到@Configuration类上,并在想要重试的方法上(或在所有方法的类型级别上)使用@Retryable,还可以指定任意数量的重试监听器。
  1. @Configuration
  2. @EnableRetry
  3. public class Application {
  4.     @Bean
  5.     public RetryListener retryListener1() {
  6.         return new RetryListener() {...}
  7.     }
  8.     @Bean
  9.     public RetryListener retryListener2() {
  10.         return new RetryListener() {...}
  11.     }
  12. }
  13. @Service
  14. class MyService {
  15.     @Retryable(RemoteAccessException.class)
  16.     public void hello() {
  17.         // ... do something
  18.     }
  19. }
复制代码
可以利用 @Retryable 的属性来控制 RetryPolicy 和 BackoffPolicy
  1. @Service
  2. public class MyService {
  3.     @Retryable(value = RuntimeException.class, maxAttempts = 5, backoff = @Backoff(value = 1000L, multiplier = 1.5))
  4.     public void sayHello() {
  5.         //  ... do something
  6.     }
  7.     @Retryable(value = {IOException.class, RemoteAccessException.class},
  8.             listeners = {"myListener1", "myListener2", "myListener3"},
  9.             maxAttempts = 5, backoff = @Backoff(delay = 100, maxDelay = 500))
  10.     public void sayHi() {
  11.         //  ... do something
  12.     }
  13.     @Retryable(maxAttempts = 5, backoff = @Backoff(delay = 1000, maxDelay = 30000, multiplier = 1.2, random = true))
  14.     public void sayBye() {
  15.         //  ... do something
  16.     }
  17. }
复制代码
如果希望在重试耗尽时执行另外的逻辑,则可以提供恢复方法。恢复方法应该在与@Retryable实例相同的类中声明,并标记为@Recover。返回类型必须匹配@Retryable方法。恢复方法的参数可以选择性地包括抛出的异常和(可选地)传递给原始可重试方法的参数(或它们的部分列表,只要在最后一个需要的参数之前没有被省略)。
  1. @Service
  2. class MyService {
  3.     @Retryable(RemoteAccessException.class)
  4.     public void service(String str1, String str2) {
  5.         // ... do something
  6.     }
  7.     @Recover
  8.     public void recover(RemoteAccessException e, String str1, String str2) {
  9.        // ... error handling making use of original args if required
  10.     }
  11. }
复制代码
为了避免多个恢复方法搞混淆了,可以手动指定用哪个恢复方法
  1. @Service
  2. class Service {
  3.     @Retryable(recover = "service1Recover", value = RemoteAccessException.class)
  4.     public void service1(String str1, String str2) {
  5.         // ... do something
  6.     }
  7.     @Retryable(recover = "service2Recover", value = RemoteAccessException.class)
  8.     public void service2(String str1, String str2) {
  9.         // ... do something
  10.     }
  11.     @Recover
  12.     public void service1Recover(RemoteAccessException e, String str1, String str2) {
  13.         // ... error handling making use of original args if required
  14.     }
  15.     @Recover
  16.     public void service2Recover(RemoteAccessException e, String str1, String str2) {
  17.         // ... error handling making use of original args if required
  18.     }
  19. }
复制代码
1.3.2及以后版本支持匹配参数化(泛型)返回类型来检测正确的恢复方法:
  1. @Service
  2. class Service {
  3.     @Retryable(RemoteAccessException.class)
  4.     public List<Thing1> service1(String str1, String str2) {
  5.         // ... do something
  6.     }
  7.     @Retryable(RemoteAccessException.class)
  8.     public List<Thing2> service2(String str1, String str2) {
  9.         // ... do something
  10.     }
  11.     @Recover
  12.     public List<Thing1> recover1(RemoteAccessException e, String str1, String str2) {
  13.        // ... error handling for service1
  14.     }
  15.     @Recover
  16.     public List<Thing2> recover2(RemoteAccessException e, String str1, String str2) {
  17.        // ... error handling for service2
  18.     }
  19. }
复制代码
1.2版本引入了对某些属性使用表达式的能力
  1. @Retryable(exceptionExpression="message.contains('this can be retried')")
  2. public void service1() {
  3.   ...
  4. }
  5. @Retryable(exceptionExpression="message.contains('this can be retried')")
  6. public void service2() {
  7.   ...
  8. }
  9. @Retryable(exceptionExpression="@exceptionChecker.shouldRetry(#root)",
  10.     maxAttemptsExpression = "#{@integerFiveBean}",
  11.     backoff = @Backoff(delayExpression = "#{1}", maxDelayExpression = "#{5}", multiplierExpression = "#{1.1}"))
  12. public void service3() {
  13.   ...
  14. }
复制代码
表达式可以包含属性占位符,比如:#{${max.delay}} 或者 #{@exceptionChecker.${retry.method}(#root)} 。规则如下:

  • exceptionExpression 以抛出的异常为根对象进行计算求值的
  • maxAttemptsExpression 和 @BackOff 表达式属性 只在初始化的时候被计算一次。它们没有用于计算的根对象,但它们可以引用上下文中的其他bean
例如:
  1. @Data
  2. @Component("runtimeConfigs")
  3. @ConfigurationProperties(prefix = "retry.cfg")
  4. public class MyRuntimeConfig {
  5.     private int maxAttempts;
  6.     private long initial;
  7.     private long max;
  8.     private double mult;
  9. }
复制代码
application.properties
  1. retry.cfg.maxAttempts=10
  2. retry.cfg.initial=100
  3. retry.cfg.max=2000
  4. retry.cfg.mult=2.0
复制代码
使用变量
  1. @Retryable(maxAttemptsExpression = "@runtimeConfigs.maxAttempts",
  2.         backoff = @Backoff(delayExpression = "@runtimeConfigs.initial",
  3.                 maxDelayExpression = "@runtimeConfigs.max", multiplierExpression = "@runtimeConfigs.mult"))
  4. public void service() {
  5.     System.out.println(LocalDateTime.now());
  6.     boolean flag = sendMsg();
  7.     if (flag) {
  8.         throw new CustomException("调用失败");
  9.     }
  10. }
  11. @Retryable(maxAttemptsExpression = "args[0] == 'something' ? 3 : 1")
  12. public void conditional(String string) {
  13.     ...
  14. }
复制代码
最后,简单看一下源码org.springframework.retry.support.RetryTemplate#doExecute()
 
RetryContext是线程局部变量

间隔时间是通过线程休眠来实现的



https://github.com/spring-projects/spring-retry


 

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

小小小幸运

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表