区分偶发性超时和频繁超时的重试策略
在实际项目中,区分偶发性超时和频繁超时的重试策略非常重要。偶发性超时可能是由于网络抖动或临时负载过高引起的,适合立即重试;而频繁超时则可能是系统过载或下游服务不可用,此时应避免重试,以免加剧问题。
在实际面试的过程中,经常会遇到类似的面试题目,这时候可以这样回答:
在处理大量请求时,我们经常会遇到超时的情况。为了合理控制重试行为,避免所谓的“重试风暴”,我设计了一个基于时间窗口的算法。在这个算法中,我们维护了一个滑动窗口,窗口内记录了每个请求的时间戳以及该请求是否超时。每当一个请求超时后,我们会统计窗口内超时的请求数量。如果超时请求的数量超过了设定的阈值,我们就认为当前系统压力较大,不适合进行重试;否则,我们认为可以安全地进行重试。
然而,随着并发量的增加,普通版的滑动窗口算法暴露出了一些问题。特别是在高并发场景下,窗口内需要维护的请求数量可能非常大,这不仅占用了大量内存,而且在判定是否需要重试时还需要遍历整个窗口,这大大增加了算法的时间复杂度。
为了解决这个问题,我们进一步设计了进阶版的算法。在这个版本中,我们引入了ring buffer 来优化滑动窗口的实现。具体来说,我们不再以时间为窗口大小,而是使用固定数量的比特位来记录请求的超时信息。每个比特位对应一个请求,用1表示超时,用0表示未超时。当所有比特位都被标记后,我们从头开始再次标记。
这种设计极大地降低了内存占用,因为无论并发量多高,我们只需要固定数量的比特位来记录请求的超时状态。同时,在判定是否需要重试时,我们只需要统计ring buffer中为1的比特数量,这大大简化了算法的实现并提高了效率。
这里涉及到以下知识点:
- 滑动窗口
- Ring Buffer
以下是针对偶发性超时和频繁超时的重试策略设计,以及如何在 Java 项目中实现。
1. 重试策略设计
偶发性超时
- 特点:短时间内少量超时,可能是临时性问题。
- 策略:
- 立即重试。
- 重试次数较少(如 1-2 次)。
- 重试间隔较短(如 100ms)。
频繁超时
- 特点:短时间内大量超时,可能是系统过载或下游服务不可用。
- 策略:
- 避免重试,直接返回错误。
- 触发熔断机制,暂时停止对下游服务的调用。
- 记录日志并告警,通知运维人员处理。
2. 实现方案
核心思路
- 使用滑动窗口(如
ring buffer)记录超时请求的比例。 - 根据超时比例动态调整重试策略:
- 如果超时比例较低(如 < 20%),认为是偶发性超时,允许重试。
- 如果超时比例较高(如 >= 20%),认为是频繁超时,禁止重试并触发熔断。
3. Java 实现
以下是完整的 Java 实现代码,包括重试策略和熔断机制。
1、创建滑动窗口统计器
使用 ring buffer 和 AtomicInteger 实现线程安全的滑动窗口。
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 控制重试行为。
@Servicepublic 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 请求拦截器。
@Configurationpublic 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 客户端时,结合熔断器策略控制请求。
@Servicepublic 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 # 半开状态下允许的请求数在代码中读取配置:
@Configurationpublic 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、熔断器状态监控
@Componentpublic 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."); / 触发告警逻辑 } }}