稳定性体系中,超时、重试、限流、熔断并称容错四大金刚,重试作为故障自愈最基础能力,用于解决 RPC 瞬时网络抖动、节点临时宕机等临时故障。但原生 OpenFeign 重试设计存在大量缺陷,线上极易引发重复下单、下游雪崩等严重故障。
设计服务提供端幂等声明 + 调用端动态配置双条件重试模型,两端权限同时生效才允许重试,从根源杜绝重复操作风险;同时配套指数退避策略、失败强制更换实例重试两大核心能力,补齐原生 Feign 全部短板。
@RpcRetryable)@RpcRetryable,允许上游重试;rpc.feign.retry)两个条件必须同时满足,接口才会执行重试;任意一个不满足,直接放弃重试,抛出原始异常。
@RpcRetryable(允许被重试)表格
服务提供端(@RpcRetryable) | 调用端(Nacos 重试开关) | 是否允许重试 | 详细说明 |
|---|---|---|---|
有注解,允许重试 | 开启 | 允许 | 两端均授权,执行换实例 + 退避重试 |
有注解,允许重试 | 关闭 | 禁止 | 调用方主动关闭自愈,直接报错 |
无注解,禁止重试 | 开启 | 禁止 | 安全兜底:下游不允许重复执行,无论调用方配置如何,一律不重试 |
无注解,禁止重试 | 关闭 | 禁止 | 两端全部关闭重试逻辑 |
如果请求失败后立刻循环重试,大量并发流量会瞬间全部打向下游故障服务,本想做故障自愈,结果直接压垮正常节点,引发雪崩故障。
解决方案:失败后等待一段时间再发起下一次重试,且等待时间指数递增:
原生 Feign 重试只会重复请求当前返回异常的实例,若该节点宕机 / 断连,所有重试全部无效,白白浪费线程资源。
本框架规则:
整套框架无侵入接入现有 Spring Cloud Feign 项目,分为四层:
@RpcRetryable,由下游服务开发标注接口幂等属性(服务端权限);业务发起 Feign 调用 → AOP 切面读取接口@RpcRetryable注解,存入上下文 → 请求出现网络 / 节点异常 → 进入自定义重试策略
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;
}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();
}
}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();
}
}
}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;
}
}
}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 "";
}
}
}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;
}
}
}
}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 {
// 自动注册切面、重试策略、退避工厂,业务无需额外配置
}yaml
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配置绑定@ConfigurationProperties,Nacos 配置变更后新请求即时生效,无需重启服务。
getUserInfo接口标注@RpcRetryable(服务端允许重试);updateUserBalance未添加@RpcRetryable;@RpcRetryable;@RpcRetryable定义接口是否可重复执行(底层安全防线),调用端 Nacos 控制是否开启自愈;二者同时生效才允许重试,杜绝写接口重复操作。下篇将介绍gRPC 双条件重试完整实现,复用同一套双向约束、指数退避、换实例重试设计;同时讲解重试指标埋点、线上告警、全量测试用例与落地踩坑避坑方案。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。