你必须懂也可以懂的微服务系列六:重试

682 阅读3分钟

1.系列文章

2.前言

网络抖动是系统调用过程中发生最频繁、持续时间较短的一种故障,如果服务调用方在出现超时情况下不做任何处理,那么就会导致某次请求失败,其实这种失败是可以通过某种手段来进行解决,也就是本文要介绍的技术重试

3.服务重试

3.1 捕获IO异常

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template);
    try {
        response = client.execute(request, options);
        // ensure the request is set. TODO: remove in Feign 12
        response = response.toBuilder()
            .request(request)
            .requestTemplate(template)
            .build();
    } catch (IOException e) {
        // 1.抛出可重试异常
        throw errorExecuting(request, e);
    }
    // ...省略部分代码
}
static FeignException errorExecuting(Request request, IOException cause) {
    return new RetryableException(
        -1,
        format("%s executing %s %s", cause.getMessage(), request.httpMethod(), request.url()),
        request.httpMethod(),
        cause,
        null, request);
}

在进行远程调用时捕获IO异常,一旦发生IO异常,对外抛出可重试异常RetryableException

3.2 重试条件判断

@Override
public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Options options = findOptions(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
        try {
            return executeAndDecode(template, options);
        } catch (RetryableException e) {
            try {
                retryer.continueOrPropagate(e);
            } catch (RetryableException th) {
                Throwable cause = th.getCause();
                if (propagationPolicy == UNWRAP && cause != null) {
                    throw cause;
                } else {
                    throw th;
                }
            }
            continue;
        }
    }
}

重试器Retryer默认实现

Retryer NEVER_RETRY = new Retryer() {
​
    @Override
    public void continueOrPropagate(RetryableException e) {
        throw e;
    }
​
    @Override
    public Retryer clone() {
        return this;
    }
};

默认情况不会进行重试,会将异常进行抛出,要想实现重试功能,需要手动指定重试器

feign:
  client:
    config: # 参考FeignClientProperties中的config
      resilience4j-provider-service: # 调用服务名称
        connectTimeout: 3000 # 连接超时时间
        readTimeout: 3000 # 读取超时时间
        retryer: feign.Retryer.Default # 指定重试策略

3.3 重试实现

public void continueOrPropagate(RetryableException e) {
    // 1.是否超过最大重试次数,如果超过则抛出异常
    if (this.attempt++ >= this.maxAttempts) {
        throw e;
    } else {
        long interval;
        // 2.获取下一次重试间隔时间
        if (e.retryAfter() != null) {
            interval = e.retryAfter().getTime() - this.currentTimeMillis();
            if (interval > this.maxPeriod) {
                interval = this.maxPeriod;
            }
​
            if (interval < 0L) {
                return;
            }
        } else {
            interval = this.nextMaxInterval();
        }
​
        try {
            // 3.休眠间隔时间
            Thread.sleep(interval);
        } catch (InterruptedException var5) {
            Thread.currentThread().interrupt();
            throw e;
        }
​
        this.sleptForMillis += interval;
    }
}

重试逻辑实现也很简单,其逻辑如下:

  • 先判断是否超过最大重试次数
  • 超过则直接抛出异常
  • 未超过则计算下次重试时间间隔
  • 休眠间隔时间后再次发起远程调用

3.4 重试自定义实现

如果默认重试器Retryer无法满足业务需求,你也可以通过自定义实现重试器来达到自己的目的

public class RpcRetryer implements Retryer {
​
    private final int maxAttempts;
    private final long period;
    private final long maxPeriod;
    int attempt;
    long sleptForMillis;
​
    public RpcRetryer() {
        this(100, SECONDS.toMillis(1), 4);
    }
​
    public RpcRetryer(long period, long maxPeriod, int maxAttempts) {
        this.period = period;
        this.maxPeriod = maxPeriod;
        this.maxAttempts = maxAttempts;
        this.attempt = 1;
    }
​
    protected long currentTimeMillis() {
        return System.currentTimeMillis();
    }
​
    @Override
    public void continueOrPropagate(RetryableException e) {
        if (attempt++ >= maxAttempts) {
            throw e;
        }
​
        long interval;
        if (e.retryAfter() != null) {
            interval = e.retryAfter().getTime() - currentTimeMillis();
            if (interval > maxPeriod) {
                interval = maxPeriod;
            }
            if (interval < 0) {
                return;
            }
        } else {
            interval = nextMaxInterval();
        }
        try {
            Thread.sleep(interval);
        } catch (InterruptedException ignored) {
            Thread.currentThread().interrupt();
            throw e;
        }
        sleptForMillis += interval;
    }
​
    /**
     * Calculates the time interval to a retry attempt. <br>
     * The interval increases exponentially with each attempt, at a rate of nextInterval *= 1.5
     * (where 1.5 is the backoff factor), to the maximum interval.
     *
     * @return time in nanoseconds from now until the next attempt.
     */
    long nextMaxInterval() {
        long interval = (long) (period * Math.pow(1.5, attempt - 1));
        return Math.min(interval, maxPeriod);
    }
​
    @Override
    public Retryer clone() {
        return new RpcRetryer(period, maxPeriod, maxAttempts);
    }
}

自定义重试器实现完成后,只需要在配置文件中进行指定即可

feign:
  client:
    config: # 参考FeignClientProperties中的config
      resilience4j-provider-service: # 调用服务名称
        retryer: com.boot.cloud.openfeign.client.retry.RpcRetryer # 指定重试策略

4. 结语

相信你在读完系列文章后会对服务注册服务反注册服务调用服务发现与负载均衡服务熔断服务重试有一个大致的认识,也希望该系列文章对你学习微服务、实践微服务有些许帮助。

5. 示例代码

boot-cloud