从放弃到入门-重试Retry是如何实现的

849 阅读2分钟

由于高并发和海量数据访问,一般系统都会从单体架构升级为集群架构,不可避免都会使用RPC框架。
但是,由于网络抖动等外部不可控因素,系统调用会偶发失败,特殊场景可能需要【重试】。
下面介绍一些常用的重试方案。

一、固定循环次数立即重试

可以查看dubbo中的com.alibaba.dubbo.rpc.cluster.support.FailoverClusterInvoker#doInvoke方法相关代码:

    public Result doInvoke(Invocation invocation, final List<Invoker<T>> invokers, LoadBalance loadbalance) throws RpcException {
        ...
        int len = getUrl().getMethodParameter(invocation.getMethodName(), Constants.RETRIES_KEY, Constants.DEFAULT_RETRIES) + 1;
        ...
        // retry loop.
        RpcException le = null; // last exception.
        List<Invoker<T>> invoked = new ArrayList<Invoker<T>>(copyinvokers.size()); // invoked invokers.
        Set<String> providers = new HashSet<String>(len);
        for (int i = 0; i < len; i++) {
            ...
            try {
                Result result = invoker.invoke(invocation);
                ...
                return result;
            } catch (RpcException e) {
                if (e.isBiz()) { // biz exception.
                    throw e;
                }
                le = e;
            } catch (Throwable e) {
                le = new RpcException(e.getMessage(), e);
            } finally {
                providers.add(invoker.getUrl().getAddress());
            }
        }
        throw new RpcException(le != null ? le.getCode() : 0);
    }

tips

dubbo常用配置如下:

<dubbo:reference id="xxxx" interface="xx" retries="3" timeout="1000"/>

其中,retries="3" 重试三次,也就是最多尝试四次,如果失败就抛出异常;
timeout="1000" 单位为毫秒,表示服务超时时间,客户端在调用该dubbo服务时会启动超时检测,如果达到1秒就会报超时异常,超时异常后客户端会再次尝试调用,如果成功就返回,如果失败继续调用、直到达到最大重试次数。 如果是timeout超时异常,会重试;如果是其他异常导致dubbo服务调用抛异常,也会立即重试,而不会等到timeout的时间才重试。
所以,一定要注意,一定要注意,一定要注意:

  • 最坏情况下:该接口最大响应时间是: (retries + 1) * timeout。

二、有固定delay的重试

在dubbo重试代码的基础上,新增一个固定delay,例如可以用Thread.sleep(delay)来模拟。

        for (int i = 0; i < len; i++) {
            ...
            try {
                Result result = invoker.invoke(invocation);
                ...
                return result;
            } catch (RpcException e) {
               Thread.sleep(delay);
            } 
        }

这种方式会造成被依赖接口间歇性的毛刺

三、带有随机delay的重试

可以看gRPC的源码,参数含义如下:

  • INITIAL_BACKOFF (how long to wait after the first failure before retrying)
  • MULTIPLIER (factor with which to multiply backoff after a failed retry)
  • JITTER (by how much to randomize backoffs).
  • MAX_BACKOFF (upper bound on backoff)
  • MIN_CONNECT_TIMEOUT (minimum time we're willing to give a connection to complete)
ConnectWithBackoff()
  current_backoff = INITIAL_BACKOFF
  current_deadline = now() + INITIAL_BACKOFF
  while (TryConnect(Max(current_deadline, now() + MIN_CONNECT_TIMEOUT))
         != SUCCESS)
    SleepUntil(current_deadline)
    current_backoff = Min(current_backoff * MULTIPLIER, MAX_BACKOFF)
    current_deadline = now() + current_backoff +
      UniformRandom(-JITTER * current_backoff, JITTER * current_backoff)

四、Spring retry库

@EnableRetry注解用于开启重试框架,必须修饰在类上

proxyTargetClass:Boolean类型,用于指明代理方式; 设置为true表示使用cglib代理,设置为false表示使用jdk动态代理,默认为false,即使用jdk动态代理

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@EnableAspectJAutoProxy(
    proxyTargetClass = false
)
@Import({RetryConfiguration.class})
@Documented
public @interface EnableRetry {
    boolean proxyTargetClass() default false;
}

@Retryable注解修饰在方法上

@Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 3000, multiplier = 2)) 设置重试3次,延迟3000毫秒再执行,每次延迟时间提高2倍。

重试策略

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retryable {
    String interceptor() default "";

    Class<? extends Throwable>[] value() default {};

    Class<? extends Throwable>[] include() default {};

    Class<? extends Throwable>[] exclude() default {};

    String label() default "";

    boolean stateful() default false;

    int maxAttempts() default 3;

    String maxAttemptsExpression() default "";

    Backoff backoff() default @Backoff;

    String exceptionExpression() default "";
}

value:只会重试抛出的指定的异常 include:作用和value类似。当exclude也为空时,默认所以异常
exclude:指定不需要处理的异常
maxAttempts:最大重试次数,默认3次
backoff:设置补偿机制

退避策略

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RetryConfiguration.class})
@Documented
public @interface Backoff {
    long value() default 1000L;

    long delay() default 0L;

    long maxDelay() default 0L;

    double multiplier() default 0.0D;

    String delayExpression() default "";

    String maxDelayExpression() default "";

    String multiplierExpression() default "";

    boolean random() default false;
}

backoff:重试等待策略
value: 设置延迟重试时间,默认为1000L,即1秒;
multiplier: 指定延迟倍数,默认为0,表示固定暂停1秒后进行重试

@Recover 当重试指定次数后,回调此方法

  • 异常类型需要与Recover方法参数类型保持一致;
  • 重试方法返回值要与recover方法返回值保持一致。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import({RetryConfiguration.class})
@Documented
public @interface Recover {
}

Spring Retry的具体源码分析可以参考:albenw.github.io/posts/69a96…