闽公网安备 35020302035485号
.超时过长:无法及时释放线程,增加系统资源耗尽风险。
通过压测或线上监控统计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: 3000
Feign 客户端定义与异常处理// 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);
}
}
}