首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >后端稳定性建设|容错四大金刚:重试(中)OpenFeign 双条件安全重试落地

后端稳定性建设|容错四大金刚:重试(中)OpenFeign 双条件安全重试落地

原创
作者头像
用户12591013
发布2026-07-21 16:15:08
发布2026-07-21 16:15:08
930
举报

一、前言

稳定性体系中,超时、重试、限流、熔断并称容错四大金刚,重试作为故障自愈最基础能力,用于解决 RPC 瞬时网络抖动、节点临时宕机等临时故障。但原生 OpenFeign 重试设计存在大量缺陷,线上极易引发重复下单、下游雪崩等严重故障。

1. 线上真实故障复盘

  1. 非幂等接口重复提交,造成资损 原生 Feign 仅 GET 允许重试,若强行开启 POST 重试,支付、下单接口超时后会重复执行,直接产生资金损失。
  2. 同实例无限重试,放大故障 默认重试只会重复请求当前故障节点,节点宕机后持续无效调用,耗尽调用方线程池。
  3. 无分层管控,一刀切策略 全局统一重试参数,查询、写接口共用一套规则,无法精细化管控风险。
  4. 缺少双向约束,风险不可控 仅调用方控制开关,服务提供方无法限制自身接口是否允许被重试,开发极易误操作。

2. OpenFeign 原生重试核心短板

  1. 区分 HTTP 方法,POST 默认不支持重试,全局开启后无安全拦截;
  2. 重试绑定当前实例,失败不会重新负载均衡切换健康节点;
  3. 仅支持全局、服务级配置,无法针对单个接口单独管控;
  4. 无服务提供方的幂等校验,单方面开启重试存在重大重复操作风险;
  5. 配置修改需要重启服务,不支持配置中心动态刷新。

3. 本文核心解决方案

设计服务提供端幂等声明 + 调用端动态配置双条件重试模型,两端权限同时生效才允许重试,从根源杜绝重复操作风险;同时配套指数退避策略失败强制更换实例重试两大核心能力,补齐原生 Feign 全部短板。

二、通用核心:双条件判定模型(Feign/gRPC 统一标准)

2.1 双条件完整释义(两段权限分离,互相约束)

条件 1:服务提供端权限(代码注解 @RpcRetryable
  • 归属方:接口提供者(下游服务)
  • 作用:定义该接口允不允许被上游调用方重试
  • 规则:
    1. 查询类、纯读、幂等接口:添加@RpcRetryable,允许上游重试;
    2. 下单、支付、扣款、数据修改等非幂等写接口:不添加该注解,永久禁止重试;
  • 核心意义:接口最清楚自身是否能重复执行,由服务端做第一道安全拦截,是最核心的兜底防线。
条件 2:调用端权限(Nacos 配置 rpc.feign.retry
  • 归属方:接口调用方(上游服务)
  • 作用:控制当前服务要不要对这个接口执行重试
  • 规则:
    1. 可按全局、服务、单接口三层开启 / 关闭重试;
    2. 可自定义重试次数、等待间隔等参数;
  • 核心意义:调用方根据自身流量、下游负载情况,灵活控制自愈策略,不需要改动下游代码。
核心判定规则

两个条件必须同时满足,接口才会执行重试;任意一个不满足,直接放弃重试,抛出原始异常。

  1. 下游接口标注@RpcRetryable(允许被重试)
  2. 当前调用服务 Nacos 配置中,该接口重试开关为 true(主动选择重试)

2.2 四场景决策矩阵

表格

服务提供端(@RpcRetryable)

调用端(Nacos 重试开关)

是否允许重试

详细说明

有注解,允许重试

开启

允许

两端均授权,执行换实例 + 退避重试

有注解,允许重试

关闭

禁止

调用方主动关闭自愈,直接报错

无注解,禁止重试

开启

禁止

安全兜底:下游不允许重复执行,无论调用方配置如何,一律不重试

无注解,禁止重试

关闭

禁止

两端全部关闭重试逻辑

2.3 配套两大核心防护能力

能力 1:指数退避策略,杜绝重试风暴

如果请求失败后立刻循环重试,大量并发流量会瞬间全部打向下游故障服务,本想做故障自愈,结果直接压垮正常节点,引发雪崩故障。

解决方案:失败后等待一段时间再发起下一次重试,且等待时间指数递增:

  1. 第一次失败:等待 100ms;
  2. 第二次失败:等待 200ms;
  3. 第三次失败:等待 400ms; 最大等待时长做封顶限制,避免单次等待过久拖慢接口响应。 错开重试时间,分散流量,防止瞬时流量冲击下游。
能力 2:失败强制更换实例重试,不在同一故障节点反复请求

原生 Feign 重试只会重复请求当前返回异常的实例,若该节点宕机 / 断连,所有重试全部无效,白白浪费线程资源。

本框架规则:

  1. 同实例最大重试次数默认 0,同一故障节点绝不重复请求
  2. 每次重试都会触发负载均衡,重新挑选一个全新健康实例发起调用;
  3. 只有切换新实例后,才会执行退避等待 + 重试逻辑,大幅提升自愈成功率。

2.4 体系统一规范(后续 gRPC 完全对齐)

  1. 双条件双向校验,服务端注解为最高安全兜底;
  2. 重试行为:同实例不重试,失败强制更换 LB 实例;
  3. 流量防护:指数退避,分散重试流量,防止下游雪崩;
  4. 配置分层:全局默认 → 服务级 → 方法级,下级覆盖上级;
  5. 配置特性:Nacos 动态刷新,修改后无需重启应用;
  6. 安全底线:未标注注解的写接口,永久关闭重试能力。

三、OpenFeign 重试整体分层架构

整套框架无侵入接入现有 Spring Cloud Feign 项目,分为四层:

  1. 注解标识层@RpcRetryable,由下游服务开发标注接口幂等属性(服务端权限);
  2. AOP 上下文层:切面拦截 Feign 调用,解析注解存入 TTL,跨线程传递幂等标识;
  3. 负载均衡重试层:自定义重试策略,同时校验「服务端注解标识 + 调用端 Nacos 配置」双条件,失败换实例 + 指数退避;
  4. 配置管理层:绑定 Nacos 配置,上游灵活控制各接口重试策略(调用端权限)。

完整调用链路

业务发起 Feign 调用 → AOP 切面读取接口@RpcRetryable注解,存入上下文 → 请求出现网络 / 节点异常 → 进入自定义重试策略

  1. 第一步校验:下游接口是否允许重试(上下文注解标识);
  2. 第二步校验:当前服务 Nacos 是否开启该接口重试;
  3. 双条件全部通过:指数退避等待,负载均衡更换新实例,重新发起请求;
  4. 任一条件不通过:直接抛出原始异常,不执行任何重试。

四、Feign 全核心组件代码实现

4.1 幂等标识注解(服务提供端使用,仅允许标注在方法上)

代码语言:javascript
复制
package com.framework.rpc.feign.annotation;

import java.lang.annotation.*;

/**
 * 服务提供端接口幂等重试标识
 * 作用:由下游服务定义该接口是否允许上游调用方重试
 * 1、查询/幂等接口添加 @RpcRetryable
 * 2、下单/支付/修改数据等非幂等接口禁止添加
 * 重试生效双条件:
 * 1、下游接口标注本注解(服务端允许重试)
 * 2、调用方Nacos配置开启对应接口重试(调用端选择重试)
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RpcRetryable {
    /**
     * true:接口幂等,允许上游调用方重试;
     * false:禁止任何上游重试
     */
    boolean value() default true;
}

4.2 TTL 上下文工具类(透传服务端注解标识)

代码语言:javascript
复制
package com.framework.rpc.feign.retry;

import com.alibaba.ttl.TransmittableThreadLocal;

/**
 * 重试上下文工具,存储下游服务是否允许重试的标识
 * 使用TTL解决线程池异步场景上下文丢失问题
 */
public class RetryContextHolder {
    private static final TransmittableThreadLocal<Boolean> SERVER_RETRY_FLAG = new TransmittableThreadLocal<>();
    private static final TransmittableThreadLocal<String> CONTEXT_ID = new TransmittableThreadLocal<>();
    private static final TransmittableThreadLocal<String> METHOD_NAME = new TransmittableThreadLocal<>();
    private static final TransmittableThreadLocal<String> FULL_METHOD_KEY = new TransmittableThreadLocal<>();

    private RetryContextHolder() {}

    // 写入服务端是否允许重试标识(AOP切面填充)
    public static void setServerRetryable(boolean retryable) {
        SERVER_RETRY_FLAG.set(retryable);
    }

    // 获取下游接口是否允许重试(重试策略读取)
    public static boolean isServerRetryable() {
        Boolean flag = SERVER_RETRY_FLAG.get();
        return flag != null && flag;
    }

    public static void setContextId(String contextId) {
        CONTEXT_ID.set(contextId);
    }

    public static String getContextId() {
        return CONTEXT_ID.get();
    }

    public static void setMethodName(String methodName) {
        METHOD_NAME.set(methodName);
    }

    public static String getMethodName() {
        return METHOD_NAME.get();
    }

    // 唯一方法key:contextId_methodName,解决同服务多接口重名冲突
    public static void setFullMethodKey(String fullKey) {
        FULL_METHOD_KEY.set(fullKey);
    }

    public static String getFullMethodKey() {
        return FULL_METHOD_KEY.get();
    }

    // 线程池复用前清理上下文,防止脏数据干扰其他请求
    public static void clear() {
        SERVER_RETRY_FLAG.remove();
        CONTEXT_ID.remove();
        METHOD_NAME.remove();
        FULL_METHOD_KEY.remove();
    }
}

4.3 AOP 切面:拦截 Feign 接口,解析服务端注解存入上下文

代码语言:javascript
复制
package com.framework.rpc.feign.retry;

import com.framework.rpc.feign.annotation.RpcRetryable;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;

@Slf4j
@Aspect
@Component
public class FeignRetryMetadataAspect {

    @Around("@within(org.springframework.cloud.openfeign.FeignClient)")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        MethodSignature signature = (MethodSignature) pjp.getSignature();
        Method method = signature.getMethod();
        Class<?> feignInterface = method.getDeclaringClass();

        // 读取下游服务接口上的 @RpcRetryable 注解(服务端权限标识)
        RpcRetryable retryAnno = method.getAnnotation(RpcRetryable.class);
        boolean serverRetryable = retryAnno != null && retryAnno.value();

        // 获取FeignClient的contextId,无则使用接口类名
        FeignClient feignClient = feignInterface.getAnnotation(FeignClient.class);
        String contextId = (feignClient != null && !feignClient.contextId().isEmpty())
                ? feignClient.contextId
                : feignInterface.getName();

        String methodName = method.getName();
        String fullMethodKey = contextId + "_" + methodName;

        // 将服务端是否允许重试的标识存入TTL上下文
        RetryContextHolder.setServerRetryable(serverRetryable);
        RetryContextHolder.setContextId(contextId);
        RetryContextHolder.setMethodName(methodName);
        RetryContextHolder.setFullMethodKey(fullMethodKey);

        if (log.isDebugEnabled()) {
            log.debug("解析Feign接口重试标记 fullKey:{}, 下游服务允许重试:{}", fullMethodKey, serverRetryable);
        }

        try {
            return pjp.proceed();
        } finally {
            // 必须清理,线程池复用时避免上下文污染
            RetryContextHolder.clear();
        }
    }
}

4.4 配置映射类:Nacos 调用端重试配置

代码语言:javascript
复制
package com.framework.rpc.feign.config;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap;
import java.util.Map;

@Data
@ConfigurationProperties(prefix = "rpc.feign.retry")
public class FeignRetryProperties {
    /** 全局总开关,调用端全局控制,默认关闭 */
    private boolean enabled = false;
    /** 同实例最大重试次数,默认0:故障节点不重复请求 */
    private int maxRetrySameInstance = 0;
    /** 更换新实例的最大重试次数(不含首次请求) */
    private int maxRetryNextInstance = 1;
    /** 指数退避最小等待毫秒 */
    private long minBackoffMs = 100;
    /** 指数退避最大等待毫秒(封顶,防止等待过久) */
    private long maxBackoffMs = 1000;
    /** 退避增长倍数 */
    private double backoffMultiplier = 2.0;

    /** 服务维度个性化配置 */
    private Map<String, ServiceRetryConfig> serviceConfigs = new HashMap<>();

    /**
     * 校验调用端是否开启当前接口重试(第二重条件)
     */
    public boolean methodEnable(String serviceId, String fullMethodKey) {
        if (!this.enabled) {
            return false;
        }
        ServiceRetryConfig serviceConfig = serviceConfigs.get(serviceId);
        if (serviceConfig == null) {
            return true;
        }
        // 优先匹配单接口精细化配置
        if (serviceConfig.getMethodConfigs().containsKey(fullMethodKey)) {
            return serviceConfig.getMethodConfigs().get(fullMethodKey).isEnabled();
        }
        return serviceConfig.isEnabled();
    }

    /** 获取接口最终生效重试参数(退避、次数等) */
    public ServiceRetryConfig getEffectiveConfig(String serviceId, String fullMethodKey) {
        ServiceRetryConfig globalDefault = buildGlobalDefault();
        ServiceRetryConfig serviceConfig = serviceConfigs.get(serviceId);
        if (serviceConfig == null) {
            return globalDefault;
        }
        if (serviceConfig.getMethodConfigs().containsKey(fullMethodKey)) {
            MethodRetryConfig methodConfig = serviceConfig.getMethodConfigs().get(fullMethodKey);
            return methodConfig.merge(globalDefault);
        }
        return serviceConfig.merge(globalDefault);
    }

    private ServiceRetryConfig buildGlobalDefault() {
        ServiceRetryConfig config = new ServiceRetryConfig();
        config.setEnabled(this.enabled);
        config.setMaxRetrySameInstance(this.maxRetrySameInstance);
        config.setMaxRetryNextInstance(this.maxRetryNextInstance);
        config.setMinBackoffMs(this.minBackoffMs);
        config.setMaxBackoffMs(this.maxBackoffMs);
        config.setBackoffMultiplier(this.backoffMultiplier);
        return config;
    }

    /** 单服务调用端重试配置 */
    @Data
    public static class ServiceRetryConfig {
        private Boolean enabled;
        private Integer maxRetrySameInstance;
        private Integer maxRetryNextInstance;
        private Long minBackoffMs;
        private Long maxBackoffMs;
        private Double backoffMultiplier;
        private Map<String, MethodRetryConfig> methodConfigs = new HashMap<>();

        public boolean isEnabled() {
            return enabled != null && enabled;
        }

        public ServiceRetryConfig merge(ServiceRetryConfig parent) {
            ServiceRetryConfig result = new ServiceRetryConfig();
            result.setEnabled(this.enabled != null ? this.enabled : parent.enabled);
            result.setMaxRetrySameInstance(this.maxRetrySameInstance != null ? this.maxRetrySameInstance : parent.maxRetrySameInstance);
            result.setMaxRetryNextInstance(this.maxRetryNextInstance != null ? this.maxRetryNextInstance : parent.maxRetryNextInstance);
            result.setMinBackoffMs(this.minBackoffMs != null ? this.minBackoffMs : parent.minBackoffMs);
            result.setMaxBackoffMs(this.maxBackoffMs != null ? this.maxBackoffMs : parent.maxBackoffMs);
            result.setBackoffMultiplier(this.backoffMultiplier != null ? this.backoffMultiplier : parent.backoffMultiplier);
            return result;
        }
    }

    /** 单个接口调用端精细化配置 */
    @Data
    public static class MethodRetryConfig {
        private Boolean enabled;
        private Integer maxRetrySameInstance;
        private Integer maxRetryNextInstance;
        private Long minBackoffMs;
        private Long maxBackoffMs;
        private Double backoffMultiplier;

        public boolean isEnabled() {
            return enabled != null && enabled;
        }

        public ServiceRetryConfig merge(ServiceRetryConfig parent) {
            ServiceRetryConfig config = new ServiceRetryConfig();
            config.setEnabled(this.enabled != null ? this.enabled : parent.enabled);
            config.setMaxRetrySameInstance(this.maxRetrySameInstance != null ? this.maxRetrySameInstance : parent.maxRetrySameInstance);
            config.setMaxRetryNextInstance(this.maxRetryNextInstance != null ? this.maxRetryNextInstance : parent.maxRetryNextInstance);
            config.setMinBackoffMs(this.minBackoffMs != null ? this.minBackoffMs : parent.minBackoffMs);
            config.setMaxBackoffMs(this.maxBackoffMs != null ? this.maxBackoffMs : parent.maxBackoffMs);
            config.setBackoffMultiplier(this.backoffMultiplier != null ? this.backoffMultiplier : parent.backoffMultiplier);
            return config;
        }
    }
}

4.5 自定义负载均衡重试策略(核心双条件判断 + 换实例逻辑)

代码语言:javascript
复制
package com.framework.rpc.feign.retry;

import com.framework.rpc.feign.config.FeignRetryProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalancedRetryPolicy;

@Slf4j
public class CustomLoadBalancedRetryPolicy extends BlockingLoadBalancedRetryPolicy {
    private final FeignRetryProperties retryProperties;
    private int sameInstanceCount = 0;
    private int nextInstanceCount = 0;

    public CustomLoadBalancedRetryPolicy(LoadBalancerProperties lbProps, FeignRetryProperties retryProperties) {
        super(lbProps);
        this.retryProperties = retryProperties;
    }

    /**
     * 双条件核心判断:同时满足才允许重试
     * 条件1:下游服务@RpcRetryable标注允许重试(服务端权限)
     * 条件2:当前调用方Nacos配置开启重试(调用端权限)
     */
    @Override
    public boolean canRetry(LoadBalancedRetryContext context) {
        // 第一层校验:服务提供端是否允许重试(最高安全兜底)
        boolean serverMark = RetryContextHolder.isServerRetryable();
        if (!serverMark) {
            log.debug("下游接口未标注@RpcRetryable,非幂等,禁止重试");
            return false;
        }
        // 第二层校验:调用端Nacos配置是否开启重试
        String serviceId = getServiceId(context);
        String fullKey = RetryContextHolder.getFullMethodKey();
        boolean clientSwitch = retryProperties.methodEnable(serviceId, fullKey);
        if (!clientSwitch) {
            log.debug("调用端未开启该接口重试 service:{}, method:{}", serviceId, fullKey);
            return false;
        }
        return true;
    }

    /** 是否重试当前故障实例,默认0次,不重复请求故障节点 */
    @Override
    public boolean canRetrySameServer(LoadBalancedRetryContext context) {
        if (!canRetry(context)) {
            return false;
        }
        FeignRetryProperties.ServiceRetryConfig config = getConfig(context);
        return sameInstanceCount < config.getMaxRetrySameInstance();
    }

    /** 是否更换全新实例重试 */
    @Override
    public boolean canRetryNextServer(LoadBalancedRetryContext context) {
        if (!canRetry(context)) {
            return false;
        }
        FeignRetryProperties.ServiceRetryConfig config = getConfig(context);
        return nextInstanceCount < config.getMaxRetryNextInstance();
    }

    /** 捕获异常后计数,达到上限切换新实例 */
    @Override
    public void registerThrowable(LoadBalancedRetryContext context, Throwable throwable) {
        if (!canRetry(context)) {
            return;
        }
        if (canRetrySameServer(context)) {
            sameInstanceCount++;
        } else {
            // 同实例重试达到上限,清空当前故障节点,下次LB重新选实例
            sameInstanceCount = 0;
            nextInstanceCount++;
            if (canRetryNextServer(context)) {
                context.setServiceInstance(null);
            } else {
                context.setExhaustedOnly();
            }
        }
    }

    private FeignRetryProperties.ServiceRetryConfig getConfig(LoadBalancedRetryContext context) {
        String serviceId = getServiceId(context);
        String fullKey = RetryContextHolder.getFullMethodKey();
        return retryProperties.getEffectiveConfig(serviceId, fullKey);
    }

    private String getServiceId(LoadBalancedRetryContext context) {
        try {
            return context.getRequest().getURI().getHost();
        } catch (Exception e) {
            log.warn("解析服务serviceId失败", e);
            return "";
        }
    }
}

4.6 自定义重试工厂:实现指数退避,分散重试流量

代码语言:javascript
复制
package com.framework.rpc.feign.retry;

import com.framework.rpc.feign.config.FeignRetryProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
import org.springframework.cloud.loadbalancer.properties.LoadBalancerProperties;
import org.springframework.retry.backoff.BackOffContext;
import org.springframework.retry.backoff.BackOffInterruptedException;
import org.springframework.retry.backoff.BackOffPolicy;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.stereotype.Component;

@Component
public class CustomRetryPolicyFactory implements LoadBalancedRetryFactory {
    private final FeignRetryProperties retryProperties;
    private final LoadBalancerProperties lbProperties;

    public CustomRetryPolicyFactory(FeignRetryProperties retryProperties, LoadBalancerProperties lbProperties) {
        this.retryProperties = retryProperties;
        this.lbProperties = lbProperties;
    }

    @Override
    public LoadBalancedRetryPolicy createRetryPolicy(String serviceId, ServiceInstanceChooser chooser) {
        return new CustomLoadBalancedRetryPolicy(lbProperties, retryProperties);
    }

    /**
     * 指数退避策略:失败后间隔递增等待,避免瞬时大量重试打垮下游
     * 不会请求失败立刻疯狂重试,错开流量,防止重试风暴
     */
    @Override
    public BackOffPolicy createBackOffPolicy(String serviceId) {
        return new MethodAwareExponentialBackOff(serviceId, retryProperties);
    }

    @Override
    public org.springframework.retry.RetryListener[] createRetryListeners(String service) {
        return new org.springframework.retry.RetryListener[0];
    }

    private static class MethodAwareExponentialBackOff implements BackOffPolicy {
        private final String serviceId;
        private final FeignRetryProperties retryProperties;

        public MethodAwareExponentialBackOff(String serviceId, FeignRetryProperties retryProperties) {
            this.serviceId = serviceId;
            this.retryProperties = retryProperties;
        }

        @Override
        public BackOffContext start(org.springframework.retry.RetryContext retryContext) {
            String fullKey = RetryContextHolder.getFullMethodKey();
            FeignRetryProperties.ServiceRetryConfig config = retryProperties.getEffectiveConfig(serviceId, fullKey);

            ExponentialBackOffPolicy delegate = new ExponentialBackOffPolicy();
            delegate.setInitialInterval(config.getMinBackoffMs());
            delegate.setMaxInterval(config.getMaxBackoffMs());
            delegate.setMultiplier(config.getBackoffMultiplier());
            return new WrapBackOffCtx(delegate.start(retryContext), delegate);
        }

        @Override
        public void backOff(BackOffContext backOffContext) throws BackOffInterruptedException {
            WrapBackOffCtx ctx = (WrapBackOffCtx) backOffContext;
            ctx.delegate.backOff(ctx.innerCtx);
        }

        private static class WrapBackOffCtx implements BackOffContext {
            final BackOffContext innerCtx;
            final ExponentialBackOffPolicy delegate;

            WrapBackOffCtx(BackOffContext innerCtx, ExponentialBackOffPolicy delegate) {
                this.innerCtx = innerCtx;
                this.delegate = delegate;
            }
        }
    }
}

4.7 Starter 自动配置类,业务零侵入接入

代码语言:javascript
复制
package com.framework.rpc.feign.config;

import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

@Configuration
@ComponentScan("com.framework.rpc.feign")
@EnableConfigurationProperties(FeignRetryProperties.class)
@LoadBalancerClients(defaultConfiguration = CustomLoadBalancerConfig.class)
public class FeignRetryAutoConfig {
    // 自动注册切面、重试策略、退避工厂,业务无需额外配置
}

五、Feign 配置规范与实操示例

5.1 配置优先级(调用端 Nacos,从高到低)

  1. 方法级唯一 key(contextId_methodName):单接口独立管控;
  2. 服务级配置:当前服务全部接口默认策略;
  3. 全局根配置:应用兜底参数。

5.2 Nacos YAML 完整配置示例(调用端控制开关与退避)

yaml

代码语言:javascript
复制
rpc:
  feign:
    retry:
      # 调用端全局总开关
      enabled: true
      # 同实例不重试
      max-retry-same-instance: 0
      # 最多更换2个新实例重试
      max-retry-next-instance: 2
      # 指数退避初始等待100ms
      min-backoff-ms: 100
      # 最长等待1000ms封顶
      max-backoff-ms: 1000
      # 等待倍数2倍
      backoff-multiplier: 2.0
      service-configs:
        user-service:
          enabled: true
          max-retry-next-instance: 3
          method-configs:
            user-api_getUserInfo:
              enabled: true
              max-retry-next-instance: 2
            # 写接口调用端直接关闭重试
            user-api_updateUserBalance:
              enabled: false

5.3 动态刷新说明

配置绑定@ConfigurationProperties,Nacos 配置变更后新请求即时生效,无需重启服务。

六、完整调用流程演示

6.1 正常自愈流程(双条件全部满足)

  1. 下游getUserInfo接口标注@RpcRetryable(服务端允许重试);
  2. 调用端 Nacos 开启该接口重试(调用端选择自愈);
  3. 首次请求访问实例 A,出现连接超时异常;
  4. 判定同实例重试次数为 0,不再请求实例 A,清空当前实例;
  5. 指数退避等待 100ms,分散流量,防止瞬时冲击;
  6. 负载均衡重新挑选全新健康实例 B 发起请求;
  7. 调用成功,返回数据。

6.2 拦截重试场景 1:下游无注解(最高安全兜底)

  1. 支付接口updateUserBalance未添加@RpcRetryable
  2. 调用端 Nacos 全局全部开启重试;
  3. 请求超时进入重试逻辑,第一层校验失败;
  4. 直接放弃重试,抛出原始异常,不会重复扣款。

6.3 拦截重试场景 2:调用端关闭开关

  1. 查询接口标注@RpcRetryable
  2. 调用端 Nacos 单独关闭该接口重试;
  3. 请求失败,第二层校验不通过,直接报错,不重试。

七、小结

7.1 三大核心设计价值复盘

  1. 双条件双向管控,安全兜底 服务端@RpcRetryable定义接口是否可重复执行(底层安全防线),调用端 Nacos 控制是否开启自愈;二者同时生效才允许重试,杜绝写接口重复操作。
  2. 指数退避,解决重试风暴 失败后阶梯式等待再重试,避免大量请求同时打向下游故障集群,防止故障放大。
  3. 更换实例重试,提升自愈成功率 不重复请求宕机 / 超时的故障节点,每次重试重新负载均衡挑选新实例,大幅提升临时故障恢复概率。

7.2 方案整体优势

  1. 安全:非幂等接口天然拦截,杜绝资损;
  2. 稳定:退避 + 换实例双重机制,不放大下游压力;
  3. 灵活:调用端分层精细化配置,动态生效;
  4. 低侵入:Starter 自动装配,业务仅需添加注解即可接入。

7.3 下篇预告

下篇将介绍gRPC 双条件重试完整实现,复用同一套双向约束、指数退避、换实例重试设计;同时讲解重试指标埋点、线上告警、全量测试用例与落地踩坑避坑方案。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、前言
    • 1. 线上真实故障复盘
    • 2. OpenFeign 原生重试核心短板
    • 3. 本文核心解决方案
  • 二、通用核心:双条件判定模型(Feign/gRPC 统一标准)
    • 2.1 双条件完整释义(两段权限分离,互相约束)
      • 条件 1:服务提供端权限(代码注解 @RpcRetryable)
      • 条件 2:调用端权限(Nacos 配置 rpc.feign.retry)
      • 核心判定规则
    • 2.2 四场景决策矩阵
    • 2.3 配套两大核心防护能力
      • 能力 1:指数退避策略,杜绝重试风暴
      • 能力 2:失败强制更换实例重试,不在同一故障节点反复请求
    • 2.4 体系统一规范(后续 gRPC 完全对齐)
  • 三、OpenFeign 重试整体分层架构
    • 完整调用链路
  • 四、Feign 全核心组件代码实现
    • 4.1 幂等标识注解(服务提供端使用,仅允许标注在方法上)
    • 4.2 TTL 上下文工具类(透传服务端注解标识)
    • 4.3 AOP 切面:拦截 Feign 接口,解析服务端注解存入上下文
    • 4.4 配置映射类:Nacos 调用端重试配置
    • 4.5 自定义负载均衡重试策略(核心双条件判断 + 换实例逻辑)
    • 4.6 自定义重试工厂:实现指数退避,分散重试流量
    • 4.7 Starter 自动配置类,业务零侵入接入
  • 五、Feign 配置规范与实操示例
    • 5.1 配置优先级(调用端 Nacos,从高到低)
    • 5.2 Nacos YAML 完整配置示例(调用端控制开关与退避)
    • 5.3 动态刷新说明
  • 六、完整调用流程演示
    • 6.1 正常自愈流程(双条件全部满足)
    • 6.2 拦截重试场景 1:下游无注解(最高安全兜底)
    • 6.3 拦截重试场景 2:调用端关闭开关
  • 七、小结
    • 7.1 三大核心设计价值复盘
    • 7.2 方案整体优势
    • 7.3 下篇预告
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档