区分偶发性超时和频繁超时的重试策略

在实际项目中,区分偶发性超时和频繁超时的重试策略非常重要。偶发性超时可能是由于网络抖动或临时负载过高引起的,适合立即重试;而频繁超时则可能是系统过载或下游服务不可用,此时应避免重试,以免加剧问题。

在实际面试的过程中,经常会遇到类似的面试题目,这时候可以这样回答:

在处理大量请求时,我们经常会遇到超时的情况。为了合理控制重试行为,避免所谓的“重试风暴”,我设计了一个基于时间窗口的算法。在这个算法中,我们维护了一个滑动窗口,窗口内记录了每个请求的时间戳以及该请求是否超时。每当一个请求超时后,我们会统计窗口内超时的请求数量。如果超时请求的数量超过了设定的阈值,我们就认为当前系统压力较大,不适合进行重试;否则,我们认为可以安全地进行重试。

然而,随着并发量的增加,普通版的滑动窗口算法暴露出了一些问题。特别是在高并发场景下,窗口内需要维护的请求数量可能非常大,这不仅占用了大量内存,而且在判定是否需要重试时还需要遍历整个窗口,这大大增加了算法的时间复杂度。

为了解决这个问题,我们进一步设计了进阶版的算法。在这个版本中,我们引入了ring buffer 来优化滑动窗口的实现。具体来说,我们不再以时间为窗口大小,而是使用固定数量的比特位来记录请求的超时信息。每个比特位对应一个请求,用1表示超时,用0表示未超时。当所有比特位都被标记后,我们从头开始再次标记。

这种设计极大地降低了内存占用,因为无论并发量多高,我们只需要固定数量的比特位来记录请求的超时状态。同时,在判定是否需要重试时,我们只需要统计ring buffer中为1的比特数量,这大大简化了算法的实现并提高了效率。

这里涉及到以下知识点:

  • 滑动窗口
  • Ring Buffer

以下是针对偶发性超时和频繁超时的重试策略设计,以及如何在 Java 项目中实现。

1. 重试策略设计

偶发性超时

  • 特点:短时间内少量超时,可能是临时性问题。
  • 策略
    • 立即重试。
    • 重试次数较少(如 1-2 次)。
    • 重试间隔较短(如 100ms)。

频繁超时

  • 特点:短时间内大量超时,可能是系统过载或下游服务不可用。
  • 策略
    • 避免重试,直接返回错误。
    • 触发熔断机制,暂时停止对下游服务的调用。
    • 记录日志并告警,通知运维人员处理。

2. 实现方案

核心思路

  • 使用滑动窗口(如 ring buffer)记录超时请求的比例。
  • 根据超时比例动态调整重试策略:
    • 如果超时比例较低(如 < 20%),认为是偶发性超时,允许重试。
    • 如果超时比例较高(如 >= 20%),认为是频繁超时,禁止重试并触发熔断。

3. Java 实现

以下是完整的 Java 实现代码,包括重试策略和熔断机制。

1、创建滑动窗口统计器

使用 ring bufferAtomicInteger 实现线程安全的滑动窗口。

public class RequestStatusTracker {
    private final int windowSize; // 滑动窗口大小
    private final double failureRateThreshold; // 失败率阈值(触发熔断)
    private final long circuitBreakerOpenTime; // 熔断器打开时间(毫秒)
    private final int halfOpenRequests; // 半开状态下允许的请求数
    private final int[] ringBuffer; // 环形缓冲区
    private final AtomicInteger currentIndex = new AtomicInteger(0); // 当前写入位置
    private final AtomicInteger successCount = new AtomicInteger(0); // 成功请求计数
    private final AtomicInteger failureCount = new AtomicInteger(0); // 失败请求计数
    private final AtomicLong circuitBreakerOpenedTime = new AtomicLong(0); // 熔断器打开时间
    private final AtomicInteger halfOpenSuccessCount = new AtomicInteger(0); // 半开状态下的成功请求计数
    private volatile CircuitBreakerState circuitBreakerState = CircuitBreakerState.CLOSED; // 熔断器状态

    // 熔断器状态枚举
    private enum CircuitBreakerState {
        CLOSED, OPEN, HALF_OPEN
    }

    public RequestStatusTracker(int windowSize, double failureRateThreshold, long circuitBreakerOpenTime, int halfOpenRequests) {
        if (windowSize <= 0 || failureRateThreshold < 0 || failureRateThreshold > 1 || circuitBreakerOpenTime <= 0 || halfOpenRequests <= 0) {
            throw new IllegalArgumentException("Invalid parameters");
        }
        this.windowSize = windowSize;
        this.failureRateThreshold = failureRateThreshold;
        this.circuitBreakerOpenTime = circuitBreakerOpenTime;
        this.halfOpenRequests = halfOpenRequests;
        this.ringBuffer = new int[windowSize]; // 0: 成功, 1: 失败
    }

    // 记录请求状态
    public void recordRequest(boolean isSuccess) {
        synchronized (this) {
            int index = currentIndex.getAndUpdate(i -> (i + 1) % windowSize);
            if (ringBuffer[index] == 0) {
                successCount.decrementAndGet();
            } else if (ringBuffer[index] == 1) {
                failureCount.decrementAndGet();
            }

            if (isSuccess) {
                ringBuffer[index] = 0;
                successCount.incrementAndGet();
            } else {
                ringBuffer[index] = 1;
                failureCount.incrementAndGet();
            }

            // 更新熔断器状态
            updateCircuitBreakerState();
        }
    }

    // 更新熔断器状态
    private void updateCircuitBreakerState() {
        double failureRate = (double) failureCount.get() / windowSize;
        long now = System.currentTimeMillis();

        switch (circuitBreakerState) {
            case CLOSED:
                if (failureRate >= failureRateThreshold) {
                    // 触发熔断
                    circuitBreakerState = CircuitBreakerState.OPEN;
                    circuitBreakerOpenedTime.set(now);
                }
                break;
            case OPEN:
                if (now - circuitBreakerOpenedTime.get() >= circuitBreakerOpenTime) {
                    // 进入半开状态
                    circuitBreakerState = CircuitBreakerState.HALF_OPEN;
                    halfOpenSuccessCount.set(0);
                }
                break;
            case HALF_OPEN:
                // 半开状态下,成功请求数达到阈值后关闭熔断器
                if (halfOpenSuccessCount.get() >= halfOpenRequests) {
                    circuitBreakerState = CircuitBreakerState.CLOSED;
                }
                break;
        }
    }

    // 判断是否允许请求
    public boolean allowRequest() {
        if (circuitBreakerState == CircuitBreakerState.OPEN) {
            return false; // 熔断器打开,禁止请求
        } else if (circuitBreakerState == CircuitBreakerState.HALF_OPEN) {
            // 半开状态下,允许部分请求
            return halfOpenSuccessCount.get() < halfOpenRequests;
        }
        return true; // 熔断器关闭,允许请求
    }

    // 获取当前熔断器状态
    public String getCircuitBreakerState() {
        return circuitBreakerState.name();
    }
  
  	// 判断熔断器是否打开
    public boolean isCircuitBreakerOpen() {
        return !CircuitBreakerState.CLOSED.equals(circuitBreakerState);
    }

    // 获取当前窗口内的失败率
    public double getFailureRate() {
        return (double) failureCount.get() / windowSize;
    }
}

熔断器状态

  • 关闭状态(Closed):正常调用下游服务。
  • 打开状态(Open):停止调用下游服务,直接返回错误。
  • 半开状态(Half-Open):尝试恢复调用,如果成功则关闭熔断器,否则继续保持打开状态。

熔断器参数

  • 失败率阈值:当失败率超过该阈值时,触发熔断。
  • 熔断时间:熔断器打开后,经过一段时间进入半开状态。
  • 恢复请求数:在半开状态下,允许尝试的请求数量。

2、在服务层调用外部 API 时,使用 RequestStatusTracker 控制重试行为。

@Service
public class UserService {

    @Autowired
    private RestTemplate restTemplate;

    @Autowired
    private RequestStatusTracker requestStatusTracker;

    // 调用外部 API 获取用户信息
    public String getUserInfo(String userId) {
        String url = "https://api.example.com/users/" + userId;
        try {
            // 发送请求
            String response = restTemplate.getForObject(url, String.class);

            // 记录请求成功(未超时)
            requestStatusTracker.recordRequest(false);
            return response;
        } catch (Exception e) {
            // 记录请求失败(超时)
            requestStatusTracker.recordRequest(true);

            // 判断是否允许重试
            if (requestStatusTracker.allowRequest()) {
                // 偶发性超时,立即重试
                return getUserInfo(userId);
            } else {
                // 频繁超时,禁止重试
                throw new RuntimeException("Request failed and retry is not allowed due to high timeout rate");
            }
        }
    }
}

3、在服务层如果是调用 Feign 客户端时,创建 Feign 请求拦截器。

@Configuration
public class FeignConfig {
    @Bean
    public ErrorDecoder errorDecoder(RequestStatusTracker tracker) {
        return (methodKey, response) -> {
            // 记录请求失败
            tracker.recordRequest(false);
            return new RuntimeException("Feign request failed with status: " + response.status());
        };
    }

    @Bean
    public feign.Logger.Level feignLoggerLevel() {
        return feign.Logger.Level.FULL; // 启用详细日志
    }
}

在 Feign 客户端中,通过 @FeignClient 注解配置拦截器。

@FeignClient(name = "userService", url = "https://api.example.com", configuration = FeignConfig.class)
public interface UserServiceClient {

    @GetMapping("/users/{userId}")
    String getUserInfo(@PathVariable String userId);
}

在服务层调用 Feign 客户端时,结合熔断器策略控制请求。

@Service
public class UserService {

    @Autowired
    private UserServiceClient userServiceClient;

    @Autowired
    private RequestStatusTracker tracker;

    public String getUserInfo(String userId) {
        // 检查是否允许请求
        if (!tracker.allowRequest()) {
            throw new RuntimeException("Circuit breaker is open. Request is not allowed.");
        }

        try {
            String response = userServiceClient.getUserInfo(userId);
            // 记录请求成功
            tracker.recordRequest(true);
            return response;
        } catch (Exception e) {
            // 记录请求失败
            tracker.recordRequest(false);
            throw new RuntimeException("Request failed: " + e.getMessage());
        }
    }
}

4、在 Spring Boot 的配置文件中,配置熔断器参数。

app:
  request:
    window-size: 100 # 滑动窗口大小
    failure-rate-threshold: 0.5 # 失败率阈值(50%)
    circuit-breaker-open-time: 10000 # 熔断器打开时间(10秒)
    half-open-requests: 5 # 半开状态下允许的请求数

在代码中读取配置:

@Configuration
public class AppConfig {

    @Value("${app.request.window-size}")
    private int windowSize;

    @Value("${app.request.failure-rate-threshold}")
    private double failureRateThreshold;

    @Value("${app.request.circuit-breaker-open-time}")
    private long circuitBreakerOpenTime;

    @Value("${app.request.half-open-requests}")
    private int halfOpenRequests;

    @Bean
    public RequestStatusTracker requestStatusTracker() {
        return new RequestStatusTracker(windowSize, failureRateThreshold, circuitBreakerOpenTime, halfOpenRequests);
    }
}

5、熔断器状态监控

@Component
public class CircuitBreakerMonitor {

    @Autowired
    private RequestStatusTracker requestStatusTracker;

    @Scheduled(fixedRate = 5000) // 每 5 秒检查一次
    public void monitor() {
        if (requestStatusTracker.isCircuitBreakerOpen()) {
            System.out.println("Circuit breaker is open! Please check the downstream service.");
            // 触发告警逻辑
        }
    }
}
Share this post:

Related content