背景
项目springcloud的版本是2021.0.8,springboot的版本是2.7.10。微服务存在一个服务生产环境部署两个节点的情况,发布的时候可以采用灰度发布,先部署一个服务,再部署另一个服务,从而保证服务高可用。但是停掉一个服务,正在处理的业务可能没处理完,建议使用kill -15来杀进程。
假设库存服务存在A节点和B节点,停掉了A节点,那么调用库存服务的订单服务就调用不通A节点,发生调用错误。springcloud默认使用loadbalancer的重试机制,今天介绍一种通过配置实现自定义重试机制。
实现
1、关掉默认的loadbalancer重试机制
spring:
cloud:
loadbalancer:
retry:
enabled: false
2、在要实现的@FeignClient注解中加入configuration = RetryConfiguration.class属性,为要自定义的重试类。
加载@FeignClient注解中是局部实现,如果想全部配置需要在RetryConfiguration类上添加@Configuration进行注入。
下面是重试的实现代码:
import feign.Request;
import feign.Retryer;
import feign.codec.ErrorDecoder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @Description: 类描述
* @Date: 2024-07-23 14:55
* @Author: gaoyufei
**/
@Slf4j
@Configuration
public class RetryConfiguration {
@Bean
public Retryer feignRetryer() {
return new CustomerRetryer();
}
@Bean
public Request.Options feignOptions() {
return new Request.Options(10000, 60000); // 连接超时和读取超时
}
@Bean
public ErrorDecoder feignError() {
return (key, response) -> {
log.info("服务返回码{},返回:{}", response.status(), response.body());
return new ErrorDecoder.Default().decode(key, response);
};
}
}
import feign.RetryableException;
import feign.Retryer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpMethod;
import static java.util.concurrent.TimeUnit.SECONDS;
/**
* @Description: 类描述
* @Date: 2024-07-23 15:07
* @Author: gaoyufei
**/
@Slf4j
public class CustomerRetryer implements Retryer {
private final int maxAttempts;//最大尝试次数,默认值为5。这包括首次请求,因此如果设置为5,则表示会尝试请求1次,并最多重试4次。
private final long period;//初始时间间隔,用于参与计算线程休眠时间。这个参数定义了重试之间的初始等待时间。
private final long maxPeriod;//线程休眠的单次最大时间上限。这个参数限制了每次重试之间的最大等待时间。
int attempt;//尝试次数,每次尝试+1。这个参数通常用于在重试逻辑中追踪尝试的次数,但并非直接配置的参数。
long sleptForMillis;//线程累计休眠总时间。这个参数是内部使用的,用于追踪线程为了重试而累计休眠的总时间,通常不需要直接配置。
public CustomerRetryer() {
this(100, SECONDS.toMillis(1), 10);
}
public CustomerRetryer(long period, long maxPeriod, int maxAttempts) {
this.period = period;
this.maxPeriod = maxPeriod;
this.maxAttempts = maxAttempts;
this.attempt = 1;
}
// visible for testing;
protected long currentTimeMillis() {
return System.currentTimeMillis();
}
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;
//新加的调用逻辑
log.info("错误内容getMessage:{},重试次数:{},重试时间间隔ms:{}",e.getCause().getMessage(),attempt,interval);
//loadbalancer.retry.enabled: false 不走loadbalancer重试
//抛出会结束调用,返回return;会重新调用
//get方法抛异常RetryableException都重新调用
if(HttpMethod.GET.matches(e.method().name())){
return;
}
//接口超时返回的e.getCause().getMessage()是timeout,服务挂了返回的是connect timed out或其他
//除了get方法,其它方法服务不能访问,status是-1,提示是connect timed out,SynchronousMethodHandler类executeAndDecode方法会重试其他节点
//其它情况,比如超时,直接抛异常不进行再次调用
if(e.status()==-1 && (e.getCause().getMessage().contains("timeout") || e.getCause().getMessage().contains("read time out"))){
throw e;
}
}
/**
* 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 milliseconds from now until the next attempt.
*/
long nextMaxInterval() {
long interval = (long) (period * Math.pow(1.5, attempt - 1));
return interval > maxPeriod ? maxPeriod : interval;
}
@Override
public CustomerRetryer clone() {
return new CustomerRetryer(period, maxPeriod, maxAttempts);
}
}
3、以上代码核心的重试逻辑是CustomerRetryer类continueOrPropagate方法,实现了Get方法都进行重试,Get之外的方法超时不进行重试,其它情况进行重试。