@FeignClient通过配置实现自定义重试机制

背景

  项目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之外的方法超时不进行重试,其它情况进行重试。

  • 5
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值