java retry(重试) spring retry, guava retrying 详解

柔情只为你懂 2022-04-18 03:05 517阅读 0赞

转载 自 http://blog.51cto.com/9250070/2156431

系列说明

java retry 的一步步实现机制。

java-retry 源码地址

情景导入

简单的需求

产品经理:实现一个按条件,查询用户信息的服务。

小明:好的。没问题。

代码

  • UserService.java

    public interface UserService {

    1. /**
    2. * 根据条件查询用户信息
    3. * @param condition 条件
    4. * @return User 信息
    5. */
    6. User queryUser(QueryUserCondition condition);

    }

  • UserServiceImpl.java

    public class UserServiceImpl implements UserService {

    1. private OutService outService;
    2. public UserServiceImpl(OutService outService) {
    3. this.outService = outService;
    4. }
    5. @Override
    6. public User queryUser(QueryUserCondition condition) {
    7. outService.remoteCall();
    8. return new User();
    9. }

    }

谈话

项目经理:这个服务有时候会失败,你看下。

小明:OutService 在是一个 RPC 的外部服务,但是有时候不稳定。

项目经理:如果调用失败了,你可以调用的时候重试几次。你去看下重试相关的东西

重试

重试作用

对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。

远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。

比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性

V1.0 支持重试版本

思考

小明:我手头还有其他任务,这个也挺简单的。5 分钟时间搞定他。

实现

  • UserServiceRetryImpl.java

    public class UserServiceRetryImpl implements UserService {

    1. @Override
    2. public User queryUser(QueryUserCondition condition) {
    3. int times = 0;
    4. OutService outService = new AlwaysFailOutServiceImpl();
    5. while (times < RetryConstant.MAX_TIMES) {
    6. try {
    7. outService.remoteCall();
    8. return new User();
    9. } catch (Exception e) {
    10. times++;
    11. if(times >= RetryConstant.MAX_TIMES) {
    12. throw new RuntimeException(e);
    13. }
    14. }
    15. }
    16. return null;
    17. }

    }

V1.1 代理模式版本

易于维护

项目经理:你的代码我看了,功能虽然实现了,但是尽量写的易于维护一点。

小明:好的。(心想,是说要写点注释什么的?)

代理模式

为其他对象提供一种代理以控制对这个对象的访问。

在某些情况下,一个对象不适合或者不能直接引用另一个对象,而代理对象可以在客户端和目标对象之间起到中介作用。

其特征是代理与委托类有同样的接口。

实现

小明想到以前看过的代理模式,心想用这种方式,原来的代码改动量较少,以后想改起来也方便些

  • UserServiceProxyImpl.java

    public class UserServiceProxyImpl implements UserService {

    1. private UserService userService = new UserServiceImpl();
    2. @Override
    3. public User queryUser(QueryUserCondition condition) {
    4. int times = 0;
    5. while (times < RetryConstant.MAX_TIMES) {
    6. try {
    7. return userService.queryUser(condition);
    8. } catch (Exception e) {
    9. times++;
    10. if(times >= RetryConstant.MAX_TIMES) {
    11. throw new RuntimeException(e);
    12. }
    13. }
    14. }
    15. return null;
    16. }

    }

V1.2 动态代理模式

方便拓展

项目经理:小明啊,这里还有个方法也是同样的问题。你也给加上重试吧。

小明:好的。

小明心想,我在写一个代理,但是转念冷静了下来,如果还有个服务也要重试怎么办呢?

  • RoleService.java

    public interface RoleService {

    1. /**
    2. * 查询
    3. * @param user 用户信息
    4. * @return 是否拥有权限
    5. */
    6. boolean hasPrivilege(User user);

    }

代码实现

  • DynamicProxy.java

    public class DynamicProxy implements InvocationHandler {

    1. private final Object subject;
    2. public DynamicProxy(Object subject) {
    3. this.subject = subject;
    4. }
    5. @Override
    6. public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    7. int times = 0;
    8. while (times < RetryConstant.MAX_TIMES) {
    9. try {
    10. // 当代理对象调用真实对象的方法时,其会自动的跳转到代理对象关联的handler对象的invoke方法来进行调用
    11. return method.invoke(subject, args);
    12. } catch (Exception e) {
    13. times++;
    14. if (times >= RetryConstant.MAX_TIMES) {
    15. throw new RuntimeException(e);
    16. }
    17. }
    18. }
    19. return null;
    20. }
    21. /**
    22. * 获取动态代理
    23. *
    24. * @param realSubject 代理对象
    25. */
    26. public static Object getProxy(Object realSubject) {
    27. // 我们要代理哪个真实对象,就将该对象传进去,最后是通过该真实对象来调用其方法的
    28. InvocationHandler handler = new DynamicProxy(realSubject);
    29. return Proxy.newProxyInstance(handler.getClass().getClassLoader(),
    30. realSubject.getClass().getInterfaces(), handler);
    31. }

    }

  • 测试代码

    @Test
    public void failUserServiceTest() {

    1. UserService realService = new UserServiceImpl();
    2. UserService proxyService = (UserService) DynamicProxy.getProxy(realService);
    3. User user = proxyService.queryUser(new QueryUserCondition());
    4. LOGGER.info("failUserServiceTest: " + user);

    }

    @Test
    public void roleServiceTest() {

    1. RoleService realService = new RoleServiceImpl();
    2. RoleService proxyService = (RoleService) DynamicProxy.getProxy(realService);
    3. boolean hasPrivilege = proxyService.hasPrivilege(new User());
    4. LOGGER.info("roleServiceTest: " + hasPrivilege);

    }

V1.3 动态代理模式增强

对话

项目经理:小明,你动态代理的方式是挺会偷懒的,可是我们有的类没有接口。这个问题你要解决一下。

小明:好的。(谁?写服务竟然不定义接口)

  • ResourceServiceImpl.java

    public class ResourceServiceImpl {

    1. /**
    2. * 校验资源信息
    3. * @param user 入参
    4. * @return 是否校验通过
    5. */
    6. public boolean checkResource(User user) {
    7. OutService outService = new AlwaysFailOutServiceImpl();
    8. outService.remoteCall();
    9. return true;
    10. }

    }

字节码技术

小明看了下网上的资料,解决的办法还是有的。

  • CGLIB

CGLIB 是一个功能强大、高性能和高质量的代码生成库,用于扩展JAVA类并在运行时实现接口。

  • javassist

javassist (Java编程助手)使Java字节码操作变得简单。

它是Java中编辑字节码的类库;它允许Java程序在运行时定义新类,并在JVM加载类文件时修改类文件。

与其他类似的字节码编辑器不同,Javassist提供了两个级别的API:源级和字节码级。

如果用户使用源代码级API,他们可以编辑类文件,而不需要了解Java字节码的规范。

整个API只使用Java语言的词汇表进行设计。您甚至可以以源文本的形式指定插入的字节码;Javassist动态编译它。

另一方面,字节码级API允许用户直接编辑类文件作为其他编辑器。

  • ASM

ASM 是一个通用的Java字节码操作和分析框架。

它可以用来修改现有的类或动态地生成类,直接以二进制形式。

ASM提供了一些通用的字节码转换和分析算法,可以从这些算法中构建自定义复杂的转换和代码分析工具。

ASM提供与其他Java字节码框架类似的功能,但主要关注性能。

因为它的设计和实现都尽可能地小和快,所以非常适合在动态系统中使用(当然也可以以静态的方式使用,例如在编译器中)。

实现

小明看了下,就选择使用 CGLIB。

  • CglibProxy.java

    public class CglibProxy implements MethodInterceptor {

    1. @Override
    2. public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
    3. int times = 0;
    4. while (times < RetryConstant.MAX_TIMES) {
    5. try {
    6. //通过代理子类调用父类的方法
    7. return methodProxy.invokeSuper(o, objects);
    8. } catch (Exception e) {
    9. times++;
    10. if (times >= RetryConstant.MAX_TIMES) {
    11. throw new RuntimeException(e);
    12. }
    13. }
    14. }
    15. return null;
    16. }
    17. /**
    18. * 获取代理类
    19. * @param clazz 类信息
    20. * @return 代理类结果
    21. */
    22. public Object getProxy(Class clazz){
    23. Enhancer enhancer = new Enhancer();
    24. //目标对象类
    25. enhancer.setSuperclass(clazz);
    26. enhancer.setCallback(this);
    27. //通过字节码技术创建目标对象类的子类实例作为代理
    28. return enhancer.create();
    29. }

    }

  • 测试

    @Test
    public void failUserServiceTest() {
    UserService proxyService = (UserService) new CglibProxy().getProxy(UserServiceImpl.class);

    User user = proxyService.queryUser(new QueryUserCondition());
    LOGGER.info(“failUserServiceTest: “ + user);
    }

    @Test
    public void resourceServiceTest() {
    ResourceServiceImpl proxyService = (ResourceServiceImpl) new CglibProxy().getProxy(ResourceServiceImpl.class);
    boolean result = proxyService.checkResource(new User());
    LOGGER.info(“resourceServiceTest: “ + result);
    }

V2.0 AOP 实现

对话

项目经理:小明啊,最近我在想一个问题。不同的服务,重试的时候次数应该是不同的。因为服务对稳定性的要求各不相同啊。

小明:好的。(心想,重试都搞了一周了,今天都周五了。)

下班之前,小明一直在想这个问题。刚好周末,花点时间写个重试小工具吧。

设计思路

  • 技术支持

spring

java 注解

  • 注解定义

注解可在方法上使用,定义需要重试的次数

  • 注解解析

拦截指定需要重试的方法,解析对应的重试次数,然后进行对应次数的重试。

实现

  • Retryable.java

    @Target({ElementType.METHOD})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface Retryable {

    1. /**
    2. * Exception type that are retryable.
    3. * @return exception type to retry
    4. */
    5. Class<? extends Throwable> value() default RuntimeException.class;
    6. /**
    7. * 包含第一次失败
    8. * @return the maximum number of attempts (including the first failure), defaults to 3
    9. */
    10. int maxAttempts() default 3;

    }

  • RetryAspect.java

    @Aspect
    @Component
    public class RetryAspect {

    1. @Pointcut("execution(public * com.github.houbb.retry.aop..*.*(..)) &&" +
    2. "@annotation(com.github.houbb.retry.aop.annotation.Retryable)")
    3. public void myPointcut() {
    4. }
    5. @Around("myPointcut()")
    6. public Object around(ProceedingJoinPoint point) throws Throwable {
    7. Method method = getCurrentMethod(point);
    8. Retryable retryable = method.getAnnotation(Retryable.class);
    9. //1. 最大次数判断
    10. int maxAttempts = retryable.maxAttempts();
    11. if (maxAttempts <= 1) {
    12. return point.proceed();
    13. }
    14. //2. 异常处理
    15. int times = 0;
    16. final Class<? extends Throwable> exceptionClass = retryable.value();
    17. while (times < maxAttempts) {
    18. try {
    19. return point.proceed();
    20. } catch (Throwable e) {
    21. times++;
    22. // 超过最大重试次数 or 不属于当前处理异常
    23. if (times >= maxAttempts ||
    24. !e.getClass().isAssignableFrom(exceptionClass)) {
    25. throw new Throwable(e);
    26. }
    27. }
    28. }
    29. return null;
    30. }
    31. private Method getCurrentMethod(ProceedingJoinPoint point) {
    32. try {
    33. Signature sig = point.getSignature();
    34. MethodSignature msig = (MethodSignature) sig;
    35. Object target = point.getTarget();
    36. return target.getClass().getMethod(msig.getName(), msig.getParameterTypes());
    37. } catch (NoSuchMethodException e) {
    38. throw new RuntimeException(e);
    39. }
    40. }

    }

方法的使用

  • fiveTimes()

当前方法一共重试 5 次。
重试条件:服务抛出 AopRuntimeExption

  1. @Override
  2. @Retryable(maxAttempts = 5, value = AopRuntimeExption.class)
  3. public void fiveTimes() {
  4. LOGGER.info("fiveTimes called!");
  5. throw new AopRuntimeExption();
  6. }
  • 测试日志

    2018-08-08 15:49:33.814 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
    2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
    2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
    2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!
    2018-08-08 15:49:33.815 INFO [main] com.github.houbb.retry.aop.service.impl.UserServiceImpl:66 - fiveTimes called!

    java.lang.reflect.UndeclaredThrowableException

V3.0 spring-retry 版本

对话

周一来到公司,项目经理又和小明谈了起来。

项目经理:重试次数是满足了,但是重试其实应该讲究策略。比如调用外部,第一次失败,可以等待 5S 在次调用,如果又失败了,可以等待 10S 再调用。。。

小明:了解。

思考

可是今天周一,还有其他很多事情要做。

小明在想,没时间写这个呀。看看网上有没有现成的。

spring-retry

Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。

在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。

还有一种方式,是开发者自己编写重试机制,但是大多不够优雅。

注解式使用

  • RemoteService.java

重试条件:遇到 RuntimeException

重试次数:3

重试策略:重试的时候等待 5S, 后面时间依次变为原来的 2 倍数。

熔断机制:全部重试失败,则调用 recover() 方法。

  1. @Service
  2. public class RemoteService {
  3. private static final Logger LOGGER = LoggerFactory.getLogger(RemoteService.class);
  4. /**
  5. * 调用方法
  6. */
  7. @Retryable(value = RuntimeException.class,
  8. maxAttempts = 3,
  9. backoff = @Backoff(delay = 5000L, multiplier = 2))
  10. public void call() {
  11. LOGGER.info("Call something...");
  12. throw new RuntimeException("RPC调用异常");
  13. }
  14. /**
  15. * recover 机制
  16. * @param e 异常
  17. */
  18. @Recover
  19. public void recover(RuntimeException e) {
  20. LOGGER.info("Start do recover things....");
  21. LOGGER.warn("We meet ex: ", e);
  22. }
  23. }
  • 测试

    @RunWith(SpringRunner.class)
    @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
    public class RemoteServiceTest {

    1. @Autowired
    2. private RemoteService remoteService;
    3. @Test
    4. public void test() {
    5. remoteService.call();
    6. }

    }

  • 日志

    2018-08-08 16:03:26.409 INFO 1433 —- [ main] c.g.h.r.spring.service.RemoteService : Call something…
    2018-08-08 16:03:31.414 INFO 1433 —- [ main] c.g.h.r.spring.service.RemoteService : Call something…
    2018-08-08 16:03:41.416 INFO 1433 —- [ main] c.g.h.r.spring.service.RemoteService : Call something…
    2018-08-08 16:03:41.418 INFO 1433 —- [ main] c.g.h.r.spring.service.RemoteService : Start do recover things….
    2018-08-08 16:03:41.425 WARN 1433 —- [ main] c.g.h.r.spring.service.RemoteService : We meet ex:

    java.lang.RuntimeException: RPC调用异常

    1. at com.github.houbb.retry.spring.service.RemoteService.call(RemoteService.java:38) ~[classes/:na]

三次调用的时间点:

  1. 2018-08-08 16:03:26.409
  2. 2018-08-08 16:03:31.414
  3. 2018-08-08 16:03:41.416

缺陷

spring-retry 工具虽能优雅实现重试,但是存在两个不友好设计:

一个是重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体,
但 sping-retry框架必须强制转换为Throwable子类。

另一个就是重试根源的断言对象使用的是 doWithRetry 的 Exception 异常实例,不符合正常内部断言的返回设计。

Spring Retry 提倡以注解的方式对方法进行重试,重试逻辑是同步执行的,重试的“失败”针对的是Throwable,
如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

@Recover 注解在使用时无法指定方法,如果一个类中多个重试方法,就会很麻烦。

注解介绍

@EnableRetry

表示是否开始重试。




















序号 属性 类型 默认值 说明
1 proxyTargetClass boolean false 指示是否要创建基于子类的(CGLIB)代理,而不是创建标准的基于Java接口的代理。

@Retryable

标注此注解的方法在发生异常时会进行重试
















































序号 属性 类型 默认值 说明
1 interceptor String “” 将 interceptor 的 bean 名称应用到 retryable()
2 value Class[] {} 可重试的异常类型。
3 label String “” 统计报告的唯一标签。如果没有提供,调用者可以选择忽略它,或者提供默认值。
4 maxAttempts int 3 尝试的最大次数(包括第一次失败),默认为3次。
5 backoff @Backoff @Backoff() 指定用于重试此操作的backoff属性。默认为空

@Backoff










































序号 属性 类型 默认值 说明
1 delay long 0 如果不设置则默认使用 1000 milliseconds 重试等待
2 maxDelay long 0 最大重试等待时间
3 multiplier long 0 用于计算下一个延迟延迟的乘数(大于0生效)
4 random boolean false 随机重试等待时间

@Recover

用于恢复处理程序的方法调用的注释。一个合适的复苏handler有一个类型为可投掷(或可投掷的子类型)的第一个参数br/>和返回与`@Retryable`方法相同的类型的值。
可抛出的第一个参数是可选的(但是没有它的方法只会被调用)。
从失败方法的参数列表按顺序填充后续的参数。

方法式使用

注解式只是让我们使用更加便捷,但是如果要更高的灵活性。可以使用各种提供的方法。

  • SimpleDemo.java

    public class SimpleDemo { private static final Logger LOGGER = LoggerFactory.getLogger(SimpleDemo.class); public static void main(String[] args) throws Exception { RetryTemplate template = new RetryTemplate(); // 策略 SimpleRetryPolicy policy = new SimpleRetryPolicy(); policy.setMaxAttempts(2); template.setRetryPolicy(policy); String result = template.execute( new RetryCallback() { @Override public String doWithRetry(RetryContext arg0) { throw new NullPointerException(); } } , new RecoveryCallback() { @Override public String recover(RetryContext context) { return “recovery callback”; } } ); LOGGER.info(“result: {}”, result); } }

  • 执行日志

    16:30:52.578 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=0 16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=1 16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry: count=1 16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Checking for rethrow: count=2 16:30:52.591 [main] DEBUG org.springframework.retry.support.RetryTemplate - Retry failed last attempt: count=2 16:30:52.592 [main] INFO com.github.houbb.retry.spring.commonway.SimpleDemo - result: recovery callback

spring-retry 结构

2018-08-08-spring-retry.jpg

概览

重试策略

重试回退策略

重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。

默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy。

guava-retrying

谈话

小华:我们系统也要用到重试

项目经理:小明前段时间用了 spring-retry,分享下应该还不错

小明:spring-retry 基本功能都有,但是必须是基于异常来进行控制。如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了。

小华:我们项目中想根据对象的属性来进行重试。你可以看下 guava-retry,我很久以前用过,感觉还不错。

小明:好的。

guava-retrying

guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。

  • 优势

guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。

Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callablecall() 方法

代码例子

入门案例

遇到异常之后,重试 3 次停止

  • HelloDemo.java

    public static void main(String[] args) {

    1. Callable<Boolean> callable = new Callable<Boolean>() {
    2. @Override
    3. public Boolean call() throws Exception {
    4. // do something useful here
    5. LOGGER.info("call...");
    6. throw new RuntimeException();
    7. }
    8. };
    9. Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
    10. .retryIfResult(Predicates.isNull())
    11. .retryIfExceptionOfType(IOException.class)
    12. .retryIfRuntimeException()
    13. .withStopStrategy(StopStrategies.stopAfterAttempt(3))
    14. .build();
    15. try {
    16. retryer.call(callable);
    17. } catch (RetryException | ExecutionException e) {
    18. e.printStackTrace();
    19. }

    }

  • 日志

    2018-08-08 17:21:12.442 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call…
    com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.
    2018-08-08 17:21:12.443 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call…
    2018-08-08 17:21:12.444 INFO [main] com.github.houbb.retry.guava.HelloDemo:41 - call…

    1. at com.github.rholder.retry.Retryer.call(Retryer.java:174)
    2. at com.github.houbb.retry.guava.HelloDemo.main(HelloDemo.java:53)

    Caused by: java.lang.RuntimeException

    1. at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:42)
    2. at com.github.houbb.retry.guava.HelloDemo$1.call(HelloDemo.java:37)
    3. at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
    4. at com.github.rholder.retry.Retryer.call(Retryer.java:160)
    5. ... 1 more

重试策略

  • ExponentialBackoff.java

重试次数:3

重试策略:固定等待 3S

  1. Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
  2. .retryIfResult(Predicates.isNull())
  3. .retryIfExceptionOfType(IOException.class)
  4. .retryIfRuntimeException()
  5. .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
  6. .withStopStrategy(StopStrategies.stopAfterAttempt(3))
  7. .build();
  8. try {
  9. retryer.call(callable);
  10. } catch (RetryException | ExecutionException e) {
  11. e.printStackTrace();
  12. }
  • 日志

    2018-08-08 17:20:41.653 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call…
    2018-08-08 17:20:44.659 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call…
    2018-08-08 17:20:47.664 INFO [main] com.github.houbb.retry.guava.ExponentialBackoff:43 - call…
    com.github.rholder.retry.RetryException: Retrying failed to complete successfully after 3 attempts.

    1. at com.github.rholder.retry.Retryer.call(Retryer.java:174)
    2. at com.github.houbb.retry.guava.ExponentialBackoff.main(ExponentialBackoff.java:56)

    Caused by: java.lang.RuntimeException

    1. at com.github.houbb.retry.guava.ExponentialBackoff$1.call(ExponentialBackoff.java:44)
    2. at com.github.houbb.retry.guava.ExponentialBackoff$1.call(ExponentialBackoff.java:39)
    3. at com.github.rholder.retry.AttemptTimeLimiters$NoAttemptTimeLimit.call(AttemptTimeLimiters.java:78)
    4. at com.github.rholder.retry.Retryer.call(Retryer.java:160)
    5. ... 1 more

guava-retrying 简介

RetryerBuilder

RetryerBuilder 是一个 factory 创建者,可以定制设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔,创建重试者 Retryer 实例。

RetryerBuilder 的重试源支持 Exception 异常对象和自定义断言对象,通过retryIfException 和 retryIfResult 设置,同时支持多个且能兼容

  • retryIfException

retryIfException,抛出 runtime 异常、checked 异常时都会重试,但是抛出 error 不会重试。

  • retryIfRuntimeException

retryIfRuntimeException 只会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试。

  • retryIfExceptionOfType

retryIfExceptionOfType 允许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error。

如:  

  1. retryIfExceptionOfType(Error.class)// 只在抛出error重试

当然我们还可以在只有出现指定的异常的时候才重试,如: 

```java 
.retryIfExceptionOfType(IllegalStateException.class)
.retryIfExceptionOfType(NullPointerException.class)

  1. 或者通过Predicate实现
  2. ```java
  3. .retryIfException(Predicates.or(Predicates.instanceOf(NullPointerException.class),
  4. Predicates.instanceOf(IllegalStateException.class)))
  • retryIfResult

retryIfResult 可以指定你的 Callable 方法在返回值的时候进行重试,如  

  1. // 返回false重试
  2. .retryIfResult(Predicates.equalTo(false))
  3. //以_error结尾才重试
  4. .retryIfResult(Predicates.containsPattern("_error$"))
  • RetryListener

当发生重试之后,假如我们需要做一些额外的处理动作,比如log一下异常,那么可以使用RetryListener。

每次重试之后,guava-retrying 会自动回调我们注册的监听。

可以注册多个RetryListener,会按照注册顺序依次调用。  

  1. .withRetryListener(new RetryListener {
  2. @Override
  3. public <T> void onRetry(Attempt<T> attempt) {
  4. logger.error("第【{}】次调用失败" , attempt.getAttemptNumber());
  5. }
  6. }
  7. )

主要接口
































































序号 接口 描述 备注
1 Attempt 一次执行任务  
2 AttemptTimeLimiter 单次任务执行时间限制 如果单次任务执行超时,则终止执行当前任务
3 BlockStrategies 任务阻塞策略 通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么),默认策略为:BlockStrategies.THREAD_SLEEP_STRATEGY
4 RetryException 重试异常  
5 RetryListener 自定义重试监听器 可以用于异步记录错误日志
6 StopStrategy 停止重试策略  
7 WaitStrategy 等待时长策略 (控制时间间隔),返回结果为下次执行时长
8 Attempt 一次执行任务
9 Attempt 一次执行任务

StopStrategy

提供三种:

  • StopAfterDelayStrategy

设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;

  • NeverStopStrategy

不停止,用于需要一直轮训知道返回期望结果的情况;

  • StopAfterAttemptStrategy

设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;

WaitStrategy

  • FixedWaitStrategy

固定等待时长策略;

  • RandomWaitStrategy

随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)

  • IncrementingWaitStrategy

递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)

  • ExponentialWaitStrategy

指数等待时长策略;

  • FibonacciWaitStrategy

Fibonacci 等待时长策略;

  • ExceptionWaitStrategy

异常时长等待策略;

  • CompositeWaitStrategy

复合时长等待策略;

总结

优雅重试共性和原理

正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介。

约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。

都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。

spring-retry 和 guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。

优雅重试适用场景

功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。

对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。

对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。

谈话

项目经理:我觉得 guava-retry 挺好的,就是不够方便。小明啊,你给封装个基于注解的吧。

小明:……

发表评论

表情:
评论列表 (有 0 条评论,517人围观)

还没有评论,来说两句吧...

相关阅读

    相关 Spring Retry机制

    在调用第三方接口或者使用mq时,会出现网络抖动,连接超时等网络异常,所以需要重试。为了使处理更加健壮并且不太容易出现故障,后续的尝试操作,有时候会帮助失败的操作最后执行成功。例