1. 概述

Spring Cloud 通过集成 Netflix Ribbon 提供了客户端负载均衡能力。而 Ribbon 的负载均衡机制还可以结合请求重试(retry)功能,进一步提升系统的容错性。

本文将深入探讨 Spring Cloud Netflix Ribbon 中的请求重试机制。

我们将从为什么需要重试讲起,然后搭建一个完整的示例应用,演示在服务短暂不可用时如何自动重试请求。整个过程会涵盖配置、依赖、实际行为观察以及重试间隔策略的定制。

2. 为什么需要重试

在微服务架构中,服务之间通过网络频繁交互。但云环境具有天然的不稳定性——网络抖动、服务短暂不可用、瞬时高负载等情况时有发生。

优雅处理失败、快速恢复,是构建高可用系统的核心原则之一。
⚠️ 很多故障是短暂的(transient)。比如一次 503 或 408 响应,稍等片刻再试可能就成功了。

通过自动重试,我们能显著提升系统的弹性(resilience),避免因短暂故障导致整个链路失败。

但也要注意:
❌ 过度重试可能加剧系统压力,甚至引发雪崩。
❌ 不合理的重试策略会增加整体延迟。

因此,重试不是“越多越好”,而是要有策略、有限制地进行

3. 环境搭建

为了演示重试机制,我们需要两个 Spring Boot 服务:

  • weather-service:提供天气信息的 REST 接口,模拟间歇性失败
  • client-service:调用 weather-service 的客户端,集成 Ribbon 并开启重试

3.1 天气服务(weather-service)

我们实现一个简单的 /weather 接口,通过配置控制其失败频率。当请求次数是某个数的倍数时,返回 503 SERVICE_UNAVAILABLE

@Value("${successful.call.divisor}")
private int divisor;
private int nrOfCalls = 0;

@GetMapping("/weather")
public ResponseEntity<String> weather() {
    LOGGER.info("Providing today's weather information");
    if (isServiceUnavailable()) {
        return new ResponseEntity<>(HttpStatus.SERVICE_UNAVAILABLE);
    }
    LOGGER.info("Today's a sunny day");
    return new ResponseEntity<>("Today's a sunny day", HttpStatus.OK);
}

private boolean isServiceUnavailable() {
    return ++nrOfCalls % divisor != 0;
}

📌 说明:

  • divisor 控制失败频率,例如设为 5 表示每 5 次请求失败 4 次
  • 日志用于观察重试行为

3.2 客户端服务(client-service)

客户端使用 Spring Cloud Netflix Ribbon 实现负载均衡和重试。

3.2.1 配置 Ribbon 客户端

@Configuration
@RibbonClient(name = "weather-service", configuration = RibbonConfiguration.class)
public class WeatherClientRibbonConfiguration {

    @LoadBalanced
    @Bean
    RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}
  • @LoadBalanced:标记 RestTemplate 启用 Ribbon 负载均衡
  • @RibbonClient:为 weather-service 定制 Ribbon 配置

3.2.2 自定义 Ribbon 策略

public class RibbonConfiguration {
 
    @Bean
    public IPing ribbonPing() {
        return new PingUrl();
    }
 
    @Bean
    public IRule ribbonRule() {
        return new RoundRobinRule();
    }
}
  • IPing:使用 PingUrl 检查服务可用性
  • IRule:使用轮询(RoundRobin)策略分发请求

3.2.3 关闭 Eureka,手动指定服务列表

由于我们不使用服务发现,需在 application.yml 中手动配置服务实例列表:

weather-service:
    ribbon:
        eureka:
            enabled: false
        listOfServers: http://localhost:8021, http://localhost:8022

3.2.4 调用远程接口

@RestController
public class MyRestController {

    @Autowired
    private RestTemplate restTemplate;

    @RequestMapping("/client/weather")
    public String weather() {
        String result = this.restTemplate.getForObject("http://weather-service/weather", String.class);
        return "Weather Service Response: " + result;
    }
}

通过 http://weather-service/weather 调用,Ribbon 会自动选择可用实例。

4. 启用重试机制

4.1 配置 application.yml

在客户端的 application.yml 中添加以下 Ribbon 重试配置:

weather-service:
  ribbon:
    MaxAutoRetries: 3
    MaxAutoRetriesNextServer: 1
    retryableStatusCodes: 503, 408
    OkToRetryOnAllOperations: true

关键参数说明:

参数 说明
MaxAutoRetries 同一实例上重试次数(默认 0)
MaxAutoRetriesNextServer 切换到其他实例的重试次数(默认 0)
retryableStatusCodes 触发重试的 HTTP 状态码列表
OkToRetryOnAllOperations 是否对所有 HTTP 方法重试(默认仅 GET)

⚠️ 注意:非幂等操作(如 POST)开启重试需谨慎,避免重复提交。

4.2 添加 Spring Retry 依赖

Ribbon 的重试功能依赖 spring-retry 库,必须显式引入:

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
</dependency>

否则重试不会生效(常见踩坑点)。

4.3 实际效果验证

启动服务

  • 启动两个 weather-service 实例:
    • 实例1:端口 8021,successful.call.divisor=5
    • 实例2:端口 8022,successful.call.divisor=2
  • 启动 client-service:端口 8080

发起请求

访问 http://localhost:8080/client/weather

观察日志

weather service instance 1:
    Providing today's weather information
    Providing today's weather information
    Providing today's weather information
    Providing today's weather information

weather service instance 2:
    Providing today's weather information
    Today's a sunny day

可以看到:

  • 实例1 连续被调用 4 次(均失败)
  • 实例2 被调用 2 次,第 2 次成功
  • 最终客户端收到成功响应

✅ 重试机制生效:先在同一实例重试,失败后切换到其他实例。

5. 重试退避策略(Backoff Policy)

5.1 默认行为

默认情况下,重试是立即进行的,没有延迟。底层使用的是 NoBackOffPolicy

但在高并发场景下,密集重试可能加剧服务压力,导致雪崩。

5.2 自定义退避策略

我们可以通过继承 RibbonLoadBalancedRetryFactory 来定制退避策略。

固定延迟(Fixed Backoff)

@Component
private class CustomRibbonLoadBalancedRetryFactory 
  extends RibbonLoadBalancedRetryFactory {

    public CustomRibbonLoadBalancedRetryFactory(
      SpringClientFactory clientFactory) {
        super(clientFactory);
    }

    @Override
    public BackOffPolicy createBackOffPolicy(String service) {
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(2000); // 2秒
        return fixedBackOffPolicy;
    }
}

指数退避(Exponential Backoff)

@Override
public BackOffPolicy createBackOffPolicy(String service) {
    ExponentialBackOffPolicy exponentialBackOffPolicy = 
      new ExponentialBackOffPolicy();
    exponentialBackOffPolicy.setInitialInterval(1000); // 初始1秒
    exponentialBackOffPolicy.setMultiplier(2);         // 倍增因子
    exponentialBackOffPolicy.setMaxInterval(10000);    // 最大10秒
    return exponentialBackOffPolicy;
}

延迟序列:1s → 2s → 4s → 8s → 10s → 10s…

随机指数退避(ExponentialRandomBackOffPolicy)

在指数退避基础上加入随机因子,避免多个客户端同时重试。

// 示例:可能产生 1.5s, 3.4s, 6.2s, 9.8s, 10s...

5.3 策略选择建议

策略 适用场景
Fixed 流量小,简单控制
Exponential 常规生产环境,逐步试探
ExponentialRandom 高并发、多客户端场景,防重试风暴

✅ **推荐生产环境使用 ExponentialRandomBackOffPolicy**,能有效分散重试压力。

6. 总结

本文系统介绍了如何在 Spring Cloud 应用中使用 Netflix Ribbon 实现失败请求的自动重试:

  • ✅ 通过配置 MaxAutoRetriesMaxAutoRetriesNextServer 控制重试次数
  • ✅ 必须引入 spring-retry 依赖,否则重试不生效
  • ✅ 可自定义 retryableStatusCodes 决定哪些错误码触发重试
  • ✅ 通过 RibbonLoadBalancedRetryFactory 定制退避策略,避免重试风暴

重试是提升系统弹性的有效手段,但需结合超时、熔断等机制综合使用,才能构建真正健壮的微服务架构。

示例代码已上传至 GitHub:https://github.com/yourname/spring-cloud-ribbon-retry-demo


原始标题:Retrying Failed Requests with Spring Cloud Netflix Ribbon