.超时过长:无法及时释放线程,增加系统资源耗尽风险。
通过压测或线上监控统计99%请求的响应时间,在此基础上增加20%-50%的缓冲(如99%响应时间为500ms,可设置超时时间为700ms-1000ms)。
@Configuration public class RestTemplateConfig { // 堆代码 duidaima.com // 连接超时时间(单位:ms):建立TCP连接的最大时间 private static final int CONNECT_TIMEOUT = 1000; // 读取超时时间(单位:ms):建立连接后,等待响应数据的最大时间 private static final int READ_TIMEOUT = 2000; @Bean public RestTemplate restTemplate() { SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory(); // 设置连接超时 factory.setConnectTimeout(CONNECT_TIMEOUT); // 设置读取超时 factory.setReadTimeout(READ_TIMEOUT); return new RestTemplate(factory); } }调用示例与异常处理
@Service public class ThirdPartyApiService { @Autowired private RestTemplate restTemplate; public String callPaymentApi(String orderId) { String apiUrl = "https://api.thirdparty.com/pay?orderId=" + orderId; try { // 发起同步请求,超时会抛出ResourceAccessException return restTemplate.getForObject(apiUrl, String.class); } catch (ResourceAccessException e) { // 超时或网络异常处理(如记录日志、返回失败状态) log.error("调用支付API超时,订单ID:{}", orderId, e); throw new BusinessException("支付请求超时,请稍后重试"); } catch (Exception e) { // 其他异常处理(如4xx参数错误、5xx服务错误) log.error("调用支付API失败,订单ID:{}", orderId, e); throw new BusinessException("支付请求失败,请检查订单信息"); } } }基于 WebClient 的超时配置
@Configuration public class WebClientConfig { // 连接超时(ms) private static final int CONNECT_TIMEOUT = 1000; // 读取超时(ms) private static final int READ_TIMEOUT = 2000; // 写入超时(ms) private static final int WRITE_TIMEOUT = 1000; @Bean public WebClient webClient() { // 基于Netty配置超时参数 HttpClient httpClient = HttpClient.create() // 连接超时 .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, CONNECT_TIMEOUT) // 读取超时:指定时间内未读取到数据则超时 .doOnConnected(conn -> conn.addHandlerLast( new ReadTimeoutHandler(READ_TIMEOUT, TimeUnit.MILLISECONDS) )) // 写入超时:指定时间内未写入数据则超时 .doOnConnected(conn -> conn.addHandlerLast( new WriteTimeoutHandler(WRITE_TIMEOUT, TimeUnit.MILLISECONDS) )); return WebClient.builder() .clientConnector(new ReactorClientHttpConnector(httpClient)) .baseUrl("https://api.thirdparty.com") // 第三方API基础路径 .build(); } }异步调用与超时处理
@Service public class AsyncThirdPartyService { @Autowired private WebClient webClient; public Mono<String> callMapApi(String address) { return webClient.get() .uri("/map/geocode?address={address}", address) .retrieve() .bodyToMono(String.class) .onErrorResume(ex -> { // 捕获超时异常(WebClientRequestException包含超时场景) if (ex instanceof WebClientRequestException && ex.getMessage().contains("timeout")) { log.error("调用地图API超时,地址:{}", address, ex); return Mono.error(new BusinessException("地图服务超时,请稍后重试")); } // 其他异常处理 log.error("调用地图API失败,地址:{}", address, ex); return Mono.error(new BusinessException("地图服务异常,请检查地址")); }); } }基于 Feign 的超时配置
feign: client: config: # 全局超时配置(default表示对所有Feign客户端生效) default: connect-timeout: 1000 # 连接超时(ms) read-timeout: 2000 # 读取超时(ms) # 局部超时配置(指定Feign客户端名称,如"payment-client") payment-client: connect-timeout: 1500 read-timeout: 3000Feign 客户端定义与异常处理
// 1. 定义Feign客户端 // name:Feign客户端名称(需与配置文件中局部配置的key一致) @FeignClient(name = "payment-client", url = "https://api.thirdparty.com") public interface PaymentFeignClient { @GetMapping("/pay") String doPayment(@RequestParam("orderId") String orderId); } // 2. 全局异常处理器(统一捕获Feign超时异常) @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(FeignException.class) public Result<?> handleFeignException(FeignException e) { // 判断是否为超时异常(Feign超时会返回504 Gateway Timeout) if (e.status() == 504) { log.error("Feign调用超时,异常信息:{}", e.getMessage(), e); return Result.fail("服务调用超时,请稍后重试"); } // 其他Feign异常(如4xx、5xx) log.error("Feign调用失败,状态码:{},异常信息:{}", e.status(), e.getMessage(), e); return Result.fail("服务调用异常,状态码:" + e.status()); } }如何高效修复瞬时故障
保证幂等性:重试前必须确保请求是幂等的(即多次调用产生的效果与一次调用一致),例如支付接口需通过订单号去重,避免重复扣款。
@Service public class RetryableApiService { @Autowired private RestTemplate restTemplate; /** * 调用第三方API并配置重试 * @param orderId 订单ID * @return API响应结果 */ @Retryable( value = {ResourceAccessException.class}, // 仅对超时异常(ResourceAccessException)重试 maxAttempts = 3, // 最大重试次数(包含首次调用,即1次首次+2次重试) backoff = @Backoff(delay = 100, multiplier = 2) // 退避策略:首次延迟100ms,后续每次翻倍(100ms→200ms→400ms) ) public String callRetryablePaymentApi(String orderId) { String apiUrl = "https://api.thirdparty.com/pay?orderId=" + orderId; log.info("第{}次调用支付API,订单ID:{}", getRetryCount(), orderId); return restTemplate.getForObject(apiUrl, String.class); } /** * 重试失败后的兜底方法(必须与@Retryable方法参数一致,且额外增加Throwable参数) * @param ex 重试过程中抛出的异常 * @param orderId 订单ID * @return 兜底返回结果 */ @Recover public String recoverPaymentApi(ResourceAccessException ex, String orderId) { log.error("支付API重试3次均失败,订单ID:{}", orderId, ex); // 兜底逻辑:如触发人工介入、记录失败日志、返回默认失败状态 return"PAY_FAILED"; } /** * 获取当前重试次数(通过Spring Retry的上下文) */ private int getRetryCount() { org.springframework.retry.support.RetrySynchronizationManagerState state = org.springframework.retry.support.RetrySynchronizationManager.getContext(); return state != null ? state.getRetryCount() + 1 : 1; } }Feign 集成 Spring Retry 的重试实现
feign: client: config: payment-client: connect-timeout: 1000 read-timeout: 2000 retry: enabled: true # 启用Feign重试 max-attempts: 3 # 最大重试次数(1次首次+2次重试) interval: 100 # 初始重试间隔(ms) max-interval: 1000 # 最大重试间隔(ms) multiplier: 2 # 间隔倍数(100ms→200ms→400ms,不超过max-interval)综合案例:超时 + 重试 + 幂等性保障
@Service public class PaymentService { @Autowired private PaymentFeignClient paymentFeignClient; @Autowired private OrderRepository orderRepository; // 订单数据库DAO /** * 支付核心方法(超时+重试+幂等性) * @param orderId 订单ID * @return 支付结果 */ @Retryable( value = {FeignException.class}, // 对Feign异常(含超时、5xx)重试 maxAttempts = 3, backoff = @Backoff(delay = 100, multiplier = 2) ) public String processPayment(String orderId) { // 1. 幂等性校验:查询订单当前状态,已支付则直接返回结果 Order order = orderRepository.findById(orderId) .orElseThrow(() -> new BusinessException("订单不存在")); if ("PAID".equals(order.getStatus())) { log.info("订单已支付,无需重复调用,订单ID:{}", orderId); return"PAID"; } // 2. 调用第三方支付API(Feign已配置超时) log.info("第{}次调用支付API,订单ID:{}", getRetryCount(), orderId); String paymentResult = paymentFeignClient.doPayment(orderId); // 3. 更新订单状态(支付成功) if ("SUCCESS".equals(paymentResult)) { order.setStatus("PAID"); orderRepository.save(order); return"支付成功"; } return"支付中"; } /** * 重试失败兜底:查询第三方API确认支付状态(避免因重试失败导致状态不一致) */ @Recover public String recoverPayment(FeignException ex, String orderId) { log.error("支付API重试失败,查询最终状态,订单ID:{}", orderId, ex); try { // 调用第三方API查询支付状态(单独配置,避免受重试影响) String status = paymentFeignClient.queryPaymentStatus(orderId); if ("SUCCESS".equals(status)) { Order order = orderRepository.findById(orderId).get(); order.setStatus("PAID"); orderRepository.save(order); return"支付成功(最终确认)"; } else { return"支付失败,请稍后查询"; } } catch (Exception e) { log.error("查询支付状态失败,订单ID:{}", orderId, e); return"支付结果未知,请联系客服"; } } private int getRetryCount() { org.springframework.retry.support.RetrySynchronizationManagerState state = org.springframework.retry.support.RetrySynchronizationManager.getContext(); return state != null ? state.getRetryCount() + 1 : 1; } }进阶
特性维度 | Spring Retry | Guava Retry |
---|---|---|
重试触发条件 | 仅支持异常触发(指定异常类型) | 支持异常触发 + 返回值触发(双重条件) |
停止策略 | 仅支持 “最大重试次数” | 支持 “最大次数 + 最大时间 + 自定义条件” 组合 |
等待策略 | 仅支持固定延迟、指数退避(简单配置) | 支持固定延迟、指数退避、随机延迟等 |
重试监听 | 无原生监听机制(需自定义切面) | 原生支持重试前 / 重试后 / 重试结束监听 |
返回值处理 | 无特殊处理(重试后直接返回结果) | 可对重试过程中的返回值做中间处理 |
@Configuration public class GuavaRetryConfig { /** * 支付API专用重试器 * 重试触发条件:1. 抛出IOException/TimeoutException;2. 返回值code为PROCESSING * 停止策略:最多重试3次 或 总耗时超5秒 * 等待策略:指数退避(100ms→200ms→400ms) */ @Bean("paymentApiRetryer") public Retryer<PaymentApiResponse> paymentApiRetryer() { return RetryerBuilder.<PaymentApiResponse>newBuilder() // 1. 异常触发重试:超时或网络异常 .retryIfExceptionOfType(TimeoutException.class) .retryIfExceptionOfType(IOException.class) // 2. 返回值触发重试:状态码为PROCESSING(处理中) .retryIfResult(response -> "PROCESSING".equals(response.getCode())) // 3. 停止策略:重试3次 或 总耗时超5秒(二者满足其一即停止) .withStopStrategy( StopStrategies.stopAfterAttemptAndTimeout( 3, // 最大重试次数(含首次调用,即1次首次+2次重试) 5, // 最大总耗时 TimeUnit.SECONDS ) ) // 4. 等待策略:指数退避,初始延迟100ms,每次翻倍,最大延迟1秒 .withWaitStrategy( WaitStrategies.exponentialWait( 100, // 初始延迟 1, // 最大延迟 TimeUnit.SECONDS ) ) // 5. 重试监听器:记录重试日志 .withRetryListener(new PaymentApiRetryListener()) .build(); } }实现重试监听器(日志与监控)
/** * 支付API重试监听器 */ public class PaymentApiRetryListener implements RetryListener { private static final Logger log = LoggerFactory.getLogger(PaymentApiRetryListener.class); /** * 每次重试前触发 */ @Override public <V> void onRetry(Attempt<V> attempt) { // 1. 获取重试次数(首次调用为0,第1次重试为1,以此类推) long retryCount = attempt.getAttemptNumber() - 1; // 2. 判断重试触发原因(异常/返回值) String triggerReason = attempt.hasException() ? "异常触发(" + attempt.getExceptionCause().getMessage() + ")" : "返回值触发(" + attempt.getResult() + ")"; // 3. 获取本次尝试耗时(毫秒) long costTime = attempt.getDelaySinceFirstAttempt().toMillis(); // 4. 记录重试日志 log.info("支付API第{}次重试,触发原因:{},累计耗时:{}ms", retryCount, triggerReason, costTime); } }业务层:使用重试器调用第三方 API
@Service public class GuavaRetryPaymentService { private static final Logger log = LoggerFactory.getLogger(GuavaRetryPaymentService.class); @Autowired private RestTemplate restTemplate; // 注入支付API专用重试器 @Autowired @Qualifier("paymentApiRetryer") private Retryer<PaymentApiResponse> paymentApiRetryer; /** * 调用第三方支付API(带Guava Retry重试逻辑) */ public PaymentApiResponse callPaymentApi(String orderId, String amount) throws ExecutionException, RetryException { // 第三方API地址(模拟) String apiUrl = "https://api.thirdparty.com/pay?orderId={1}&amount={2}"; try { // 执行带重试的API调用:retryer会自动根据配置的策略重试 return paymentApiRetryer.call(() -> { // 1. 发起API请求(此处模拟不同场景的返回结果) PaymentApiResponse response = mockThirdPartyPaymentApi(orderId, amount); // 2. 模拟可能抛出的异常(超时/网络异常) if ("TIMEOUT".equals(response.getCode())) { throw new TimeoutException("支付API超时,订单ID:" + orderId); } if ("NETWORK_ERROR".equals(response.getCode())) { throw new IOException("支付API网络异常,订单ID:" + orderId); } // 3. 返回正常响应(Retryer会根据返回值判断是否重试) return response; }); } catch (ExecutionException e) { // 封装异常信息(ExecutionException是Guava Retry的外层异常,需解析原始异常) log.error("支付API重试后仍失败,订单ID:{},原始异常:{}", orderId, e.getCause().getMessage(), e); throw e; // 向上抛出,由全局异常处理器处理 } catch (RetryException e) { // 重试达到停止条件(次数/时间)仍失败 log.error("支付API达到最大重试限制,订单ID:{},重试次数:{}", orderId, e.getNumberOfFailedAttempts()); throw e; } } /** * 模拟第三方支付API的返回结果(用于测试不同场景) * 实际项目中替换为真实的restTemplate.getForObject()/postForObject() */ private PaymentApiResponse mockThirdPartyPaymentApi(String orderId, String amount) { // 场景1:第1次调用返回PROCESSING(触发返回值重试) // 场景2:第2次调用抛出TimeoutException(触发异常重试) // 场景3:第3次调用返回SUCCESS(成功,不重试) long retryCount = paymentApiRetryer.toString().contains("attempt=1") ? 1 : paymentApiRetryer.toString().contains("attempt=2") ? 2 : 3; if (retryCount == 1) { return new PaymentApiResponse("PROCESSING", "支付处理中", orderId); } elseif (retryCount == 2) { return new PaymentApiResponse("TIMEOUT", "支付超时", orderId); } else { return new PaymentApiResponse("SUCCESS", "支付成功", orderId); } } }