原因

公司项目使用Spring Cloud做后端架构,后端接口间调用使用的是Feign。前几天在生产上发现了一个问题,定时任务会重复执行。分析了日志后,得出结论,因为定时任务执行时间较长,定时任务模块调用业务模块接口会等到超时,然后Spring Cloud 的Feign会重试请求,导致定时任务执行两次。

百度查到的都不能解决我遇到的问题,我下决心要看源码透彻了解这个问题。

以下不完全是上述问题的解决,也有对Spring其他源码的理解,若嫌篇幅长,可直接翻到底看解决方案。

分析Feign

既然大致确定是因为Feign导致请求重发,我们就从分析Feign的重试来着手。

Feign有个Retryer类来控制请求的重试,Retryer是个interface,有两个实现,一个是Retryer.Default,一个是Retryer.NEVER_RETRY。看源码我们得知,Retryer.Default是重试5次,Retryer.NEVER_RETRY是不重试,那Feign默认使用的事哪个实现呢?

获取Retryer

在spring的项目中,所有的Bean都是通过Spring容器来管理的。先来看看从Spring管理的bean中能否获取到Retryer,获取到的Retryer是哪个。首先在注解EnableFeignClients有个注解@Import(FeignClientsRegistrar.class),它引入了FeignClientsRegistrar这个配置类。

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients {
//...

在FeignClientsRegistrar的registerBeanDefinitions方法中执行了方法registerFeignClients。

class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, BeanClassLoaderAware, EnvironmentAware {
    @Override
    public void registerBeanDefinitions(AnnotationMetadata metadata,
            BeanDefinitionRegistry registry) {
        registerDefaultConfiguration(metadata, registry);
        registerFeignClients(metadata, registry);
    }
//...

 以下是registerFeignClients关键代码,在registerFeignClients方法中,将FeignClientFactoryBean注册到了spring容器中

private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
    String className = annotationMetadata.getClassName();
    BeanDefinitionBuilder definition = BeanDefinitionBuilder
            .genericBeanDefinition(FeignClientFactoryBean.class);
    // ...
    AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
    BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
            new String[] { alias });
    BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
}

FeignClientFactoryBean实现了ApplicationContextAware接口,会执行getObject方法,并将返回结果注入到Spring容器中,关键代码如下

class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
        ApplicationContextAware {
    @Override
    public Object getObject() throws Exception {
        FeignContext context = applicationContext.getBean(FeignContext.class);
        Feign.Builder builder = feign(context);
        // ...
        Targeter targeter = get(context, Targeter.class);
        return targeter.target(this, builder, context, new HardCodedTarget<>(
                this.type, this.name, url));
    }

先来看看 return targeter.target 这个方法,这个方法最终会返回一个Feign的实例,并最终注入到Spring的容器中

再来看getObject方法中的 Feign.Builder builder = feign(context); ,这个方法最终会执行方法configureUsingConfiguration,configureUsingConfiguration的关键代码如下:

protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
    //...
    Retryer retryer = getOptional(context, Retryer.class);
    if (retryer != null) {
    	builder.retryer(retryer);
    }
    //...
}

这里获取了Retryer的实例,我们可以在此打断点,然后使用idea的Evluation Expression执行 retryer == Retryer.NEVER_RETRY,执行结果是true,说明Feign默认使用的是Retryer.NEVER_RETRY。那么为什么还是会重复发送请求呢,先来看看spring是怎么注入Retryer.NEVER_RETRY是怎么注入到Spring容器中的。

注入Retryer

在spring.factories中有org.springframework.cloud.netflix.feign.FeignAutoConfiguration,在FeignAutoConfiguration中实例化了FeignContext

@Configuration
@ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class})
public class FeignAutoConfiguration {
    @Bean
    public FeignContext feignContext() {
        FeignContext context = new FeignContext();
        context.setConfigurations(this.configurations);
        return context;
    }
    //...

FeignContext.java:

public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
    public FeignContext() {
        super(FeignClientsConfiguration.class, "feign", "feign.client.name");
    }
}

以下是NamedContextFactory.java中的关键代码

public abstract class NamedContextFactory<C extends NamedContextFactory.Specification>
        implements DisposableBean, ApplicationContextAware {
    private Class<?> defaultConfigType;
    public NamedContextFactory(Class<?> defaultConfigType, String propertySourceName,
            String propertyName) {
        //设置加载的配置类,此处为FeignClientsConfiguration.class
        this.defaultConfigType = defaultConfigType;
        this.propertySourceName = propertySourceName;
        this.propertyName = propertyName;
    }
    @Override
    public void setApplicationContext(ApplicationContext parent) throws BeansException {
        this.parent = parent;
    }
    protected AnnotationConfigApplicationContext createContext(String name) {
        //...
        context.register(PropertyPlaceholderAutoConfiguration.class,
                this.defaultConfigType); //注册配置类到当前容器中,
        //...
        if (this.parent != null) {
            // 设置当前的context为applicationContxt的子context
            context.setParent(this.parent);
        }
        // 刷新context,子容器的初始化    //父子容器查看DefaultListableBeanFactory.java的344行getBean方法
        context.refresh();
        return context;
    }

这里的context.refresh()会初始化当前容器,也就是将FeignClientsConfiguration中所有的配置Bean加载到当前容器中。

再来看看FeignClientsConfiguration.java这个配置类,这里初始化了很多Bean,其中有这么一段,也就是注入到spring的Retryer实例

@Bean
@ConditionalOnMissingBean
public Retryer feignRetryer() {
    return Retryer.NEVER_RETRY;
}

既然Feign默认使用的是Retryer.NEVER_RETRY。那么为什么还是会重复发送请求呢?

看完注入和获取实例的代码没有解决问题,只能再看看调用接口的代码了。

分析Ribbon

调用接口的代码在SynchronousMethodHandler的invoke方法中

public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
        try {
            return executeAndDecode(template);
        } catch (RetryableException e) {
            retryer.continueOrPropagate(e);
            if (logLevel != Logger.Level.NONE) {
                logger.logRetry(metadata.configKey(), logLevel);
            }
            continue;
        }
    }
}

这里使用了Feign的Retryer对象,上面已经分析过,这里的retryer是Retryer.NEVER_RETRY,也就是在catch的时候不会重试请求,会直接跳出这个while循环。再来看看executeAndDecode方法,

Object executeAndDecode(RequestTemplate template) throws Throwable {
    Request request = targetRequest(template);
    Response response = client.execute(request, options);
//...

executeAndDecode方法中,这里的client有两个实现类,Client.Default和LoadBalancerFeignClient,来看看这里用的是哪个

在spring.factories中,有org.springframework.cloud.netflix.feign.ribbon.FeignRibbonClientAutoConfiguration,FeignRibbonClientAutoConfiguration在引入了DefaultFeignLoadBalancedConfiguration.class

@Import({ HttpClientFeignLoadBalancedConfiguration.class,
		OkHttpFeignLoadBalancedConfiguration.class,
		DefaultFeignLoadBalancedConfiguration.class })
public class FeignRibbonClientAutoConfiguration {
//...

DefaultFeignLoadBalancedConfiguration.java代码如下

@Configuration
class DefaultFeignLoadBalancedConfiguration {
    @Bean
    @ConditionalOnMissingBean
    public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
                  SpringClientFactory clientFactory) {
        return new LoadBalancerFeignClient(new Client.Default(null, null),
            cachingFactory, clientFactory);
    }
}

这段代码将LoadBalancerFeignClient注入到了Spring容器中,上面那个client的实现类就是LoadBalancerFeignClient。也就是说只要引入spring-cloud-feign这个jar包,那Ribbon就是使用LoadBalancerFeignClient作为发起接口调用请求的实现类

再来看LoadBalancerFeignClient的execute方法,一层层看,发现他调用了LoadBalancerCommand的submit方法。这里有两个参数:

final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();//同一个server的重试次数
final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();//下一个server的重试次数

看到这里,终于明白是什么参数在导致请求重发的问题,maxRetrysSame表示对同一个server的请求的最大重试次数,maxRetrysNext表示对相同服务的不同节点的请求的最大重试次数。

往下看一层,发现在RequestSpecificRetryHandler.java,获取这两个参数的关键代码如下:

if (requestConfig != null) {
    if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetries)) {
        retrySameServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetries); 
    }
    if (requestConfig.containsProperty(CommonClientConfigKey.MaxAutoRetriesNextServer)) {
        retryNextServer = requestConfig.get(CommonClientConfigKey.MaxAutoRetriesNextServer); 
    } 
}

这段逻辑就是从配置文件中获取retrySameServer和retrySameServer,若配置文件中没有,则获取默认值

然后看DefaultClientConfigImpl.java,发现这两个参数有默认值

public static final IClientConfigKey<Integer> MaxAutoRetries = new CommonClientConfigKey<Integer>("MaxAutoRetries"){};
public static final IClientConfigKey<Integer> MaxAutoRetriesNextServer = new CommonClientConfigKey<Integer>("MaxAutoRetriesNextServer"){};


public static final int DEFAULT_MAX_AUTO_RETRIES_NEXT_SERVER = 1;
public static final int DEFAULT_MAX_AUTO_RETRIES = 0;

看到这里,终于明白ribbon重试的机制,于是只需要配置这两个参数即可停止接口调用的重试

解决

在配置文件中增加以下配置,解决了会重发请求的问题

ribbon.MaxAutoRetries=0
ribbon.MaxAutoRetriesNextServer=0

结语

分析了一大堆,其实有很多是和接口调用重试无关的,相当于是把这几天所有源码分析的过程都记录下来了,也是想更加巩固Spring和Spring cloud许多底层只是,包括Bean注入,Bean获取,父子容器初始化,接口调用,重试机制等知识。