最近需要对接很多第三方接口,有些接口因数据量大导致超时,有些因网络不稳定导致接口异常。在开发阶段还可以看日志、debugger处理,生产环境则需要更完善的方式处理。

重试的基本原则
在实现重试机制前,我们需要明确几个基本原则:
- 不是所有错误都适合重试:例如参数错误、认证失败等客户端错误通常不适合重试
- 重试间隔应该递增:避免对目标系统造成雪崩效应
- 设置最大重试次数:防止无限重试导致资源耗尽
- 考虑幂等性:确保重试不会导致业务逻辑重复执行
基础实现:自定义重试逻辑
让我们从一个基础的实现开始,逐步改进我们的重试机制:
/**
* 基础重试工具类
*/
public class RetryUtil {
/**
* 执行带重试的操作
* @param operation 需要执行的操作
* @param maxAttempts 最大尝试次数
* @param initialDelayMs 初始延迟时间(毫秒)
* @param maxDelayMs 最大延迟时间(毫秒)
* @param retryableExceptions 需要重试的异常类
* @param 返回值类型
* @return 操作执行的结果
* @throws Exception 如果重试后仍然失败
*/
public static T executeWithRetry(
Supplier operation,
int maxAttempts,
long initialDelayMs,
long maxDelayMs,
Set<Class> retryableExceptions) throws Exception {
int attempts = 0;
long delay = initialDelayMs;
Exception lastException = null;
while (attempts < maxattempts try return operation.get catch exception e lastexception='e;' boolean isretryable='retryableExceptions.stream()' .anymatchexclass -> exClass.isAssignableFrom(e.getClass()));
if (!isRetryable || attempts == maxAttempts - 1) {
throw e; // 不可重试或已达到最大重试次数
}
// 记录重试信息
System.out.printf("操作失败,准备第%d次重试,延迟%dms,异常:%s%n",
attempts + 1, delay, e.getMessage());
try {
// 线程休眠,实现重试延迟
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试过程被中断", ie);
}
// 计算下一次重试的延迟时间(指数退避)
delay = Math.min(delay * 2, maxDelayMs);
attempts++;
}
}
// 这里通常不会执行到,因为最后一次失败时会直接抛出异常
throw new RuntimeException("达到最大重试次数后仍然失败", lastException);
}
/**
* 使用示例
*/
public static void main(String[] args) {
try {
// 定义可重试的异常类型
Set<Class> retryableExceptions = new HashSet<>();
retryableExceptions.add(IOException.class);
retryableExceptions.add(SocketTimeoutException.class);
// 执行HTTP请求并带有重试机制
String result = executeWithRetry(
() -> callExternalApi("https://api.example.com/data"),
3, // 最多重试3次
1000, // 初始延迟1秒
5000, // 最大延迟5秒
retryableExceptions
);
System.out.println("API调用成功,结果:" + result);
} catch (Exception e) {
System.err.println("最终调用失败:" + e.getMessage());
}
}
/**
* 模拟调用外部API
* @param url API地址
* @return API返回结果
* @throws IOException 如果API调用失败
*/
private static String callExternalApi(String url) throws IOException {
// 这里是实际的API调用逻辑
// 示例中简化处理,随机模拟成功或失败
if (Math.random() < 0.7) { // 70%的概率失败
throw new SocketTimeoutException("连接超时");
}
return "模拟API返回数据";
}
}
上面的代码实现了一个基础的重试机制,具有以下特点:
- 支持设置最大重试次数
- 使用指数退避算法增加重试间隔
- 可以指定哪些异常类型需要重试
- 透明地处理重试逻辑,对调用方几乎无感知
使用Spring Retry提升重试能力
虽然自定义实现能够满足基本需求,但在企业级应用中,我们通常会使用成熟的框架来处理重试逻辑。Spring Retry是一个功能强大的重试框架,它提供了更加丰富的配置选项和更好的集成能力。
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
import java.io.IOException;
import java.net.SocketTimeoutException;
/**
* 使用Spring Retry实现的API调用服务
*/
@Service
public class ExternalApiService {
private final RestTemplate restTemplate = new RestTemplate();
/**
* 调用外部支付API
*
* @param orderId 订单ID
* @param amount 金额
* @return 支付结果
* @throws IOException 如果API调用失败
*/
@Retryable(
value = {IOException.class, SocketTimeoutException.class}, // 指定需要重试的异常
maxAttempts = 3, // 最大尝试次数(包括第一次)
backoff = @Backoff(delay = 1000, multiplier = 2, maxDelay = 5000) // 重试延迟策略
)
public PaymentResult processPayment(String orderId, double amount) throws IOException {
System.out.println("尝试处理支付,订单ID: " + orderId + ", 金额: " + amount);
// 这里是实际的API调用代码
// 为了演示,我们模拟一个可能失败的API调用
if (Math.random() < 0.7) { // 70%的概率失败
throw new SocketTimeoutException("支付网关连接超时");
}
// 支付成功
return new PaymentResult(orderId, "SUCCESS", "TXN" + System.currentTimeMillis());
}
/**
* 恢复方法,当重试达到最大次数后调用
*
* @param e 导致失败的异常
* @param orderId 订单ID
* @param amount 金额
* @return 支付结果(失败状态)
*/
@Recover
public PaymentResult recoverPayment(Exception e, String orderId, double amount) {
System.err.println("支付处理失败,已达到最大重试次数,订单ID: " + orderId);
// 记录失败日志,可能需要人工干预或其他补偿措施
return new PaymentResult(orderId, "FAILED", null);
}
/**
* 支付结果类
*/
public static class PaymentResult {
private final String orderId;
private final String status;
private final String transactionId;
public PaymentResult(String orderId, String status, String transactionId) {
this.orderId = orderId;
this.status = status;
this.transactionId = transactionId;
}
// getter方法省略...
@Override
public String toString() {
return "PaymentResult{" +
"orderId='" + orderId + ''' +
", status='" + status + ''' +
", transactionId='" + transactionId + ''' +
'}';
}
}
}
/**
* Spring Boot配置类,启用重试功能
*/
@Configuration
@EnableRetry
public class RetryConfig {
// 可以在这里添加更多的重试配置
}
Spring Retry的优势在于:
- 声明式配置:通过注解即可完成重试配置,代码更加简洁
- 统一管理:可以在配置类中集中管理重试策略
- 恢复机制:提供了@Recover注解用于处理最终失败的情况
- 丰富的策略:支持多种重试策略和回退策略
高级重试策略:断路器模式
在处理第三方接口时,有时我们需要更复杂的重试策略。例如,当接口持续失败时,继续重试可能会浪费资源并延长响应时间。断路器模式可以解决这个问题。
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig.SlidingWindowType;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryConfig;
import java.io.IOException;
import java.time.Duration;
import java.util.function.Supplier;
/**
* 使用Resilience4j实现断路器和重试组合
*/
public class ResilientApiClient {
private final CircuitBreaker circuitBreaker;
private final Retry retry;
public ResilientApiClient() {
// 配置断路器
CircuitBreakerConfig circuitBreakerConfig = CircuitBreakerConfig.custom()
.failureRateThreshold(50) // 当失败率超过50%时断路
.waitDurationInOpenState(Duration.ofSeconds(30)) // 断路器打开后等待30秒
.slidingWindowType(SlidingWindowType.COUNT_BASED) // 基于请求数量的滑动窗口
.slidingWindowSize(10) // 滑动窗口大小为10个请求
.build();
this.circuitBreaker = CircuitBreaker.of("paymentApi", circuitBreakerConfig);
// 配置重试
RetryConfig retryConfig = RetryConfig.custom()
.maxAttempts(3) // 最大尝试次数
.waitDuration(Duration.ofMillis(1000)) // 初始等待时间
.retryExceptions(IOException.class) // 需要重试的异常
.exponentialBackoff() // 使用指数退避策略
.build();
this.retry = Retry.of("paymentApi", retryConfig);
}
/**
* 调用支付API,结合断路器和重试功能
*
* @param orderId 订单ID
* @param amount 金额
* @return 支付结果
*/
public String processPayment(String orderId, double amount) {
// 创建供应商函数,封装实际的API调用
Supplier paymentSupplier = () -> {
System.out.println("处理支付,订单ID: " + orderId + ", 金额: " + amount);
// 这里是实际的API调用逻辑
if (Math.random() < 0.7) { // 70%的概率失败
throw new IOException("支付网关连接超时");
}
return "支付成功,交易ID: TXN" + System.currentTimeMillis();
};
// 将断路器和重试功能应用到支付调用
// 先应用重试,再应用断路器
Supplier resilientSupplier = Retry.decorateSupplier(retry,
CircuitBreaker.decorateSupplier(circuitBreaker,
paymentSupplier));
try {
// 执行带有弹性的支付调用
return resilientSupplier.get();
} catch (Exception e) {
System.err.println("支付处理最终失败: " + e.getMessage());
// 检查断路器状态
if (circuitBreaker.getState() == CircuitBreaker.State.OPEN) {
return "支付服务暂时不可用,请稍后重试";
} else {
return "支付处理失败: " + e.getMessage();
}
}
}
/**
* 使用示例
*/
public static void main(String[] args) {
ResilientApiClient client = new ResilientApiClient();
// 模拟多次调用,观察断路器行为
for (int i = 0; i < 20; i++) {
System.out.println("=== 第" + (i + 1) + "次调用 ===");
String result = client.processPayment("ORDER-" + i, 100.0 * i);
System.out.println("结果: " + result);
System.out.println("断路器状态: " + client.circuitBreaker.getState());
System.out.println();
try {
Thread.sleep(500); // 短暂延迟,便于观察
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
通用重试框架:灵活且强大的解决方案
为了应对各种复杂场景,我们可能需要更加灵活的重试框架。上面的代码实现了一个通用重试框架,它具有以下特点:
Builder模式:使用Builder模式构建重试配置,使API更加直观 灵活的策略配置:支持配置最大尝试次数、初始延迟、最大延迟、退避乘数等 多种退避策略:支持固定延迟和指数退避两种策略 可定制的异常处理:可以精确控制哪些异常需要重试 日志记录:通过接口抽象,支持可定制的日志记录方式 链式调用:API设计支持链式调用,提升代码可读性
/**
* 通用重试框架
* 提供灵活的重试策略配置和全局异常处理
*/
public class GenericRetryFramework {
/**
* 重试策略配置类
*/
public static class RetryConfig {
private int maxAttempts = 3; // 最大尝试次数
private long initialDelayMs = 1000; // 初始延迟时间(毫秒)
private long maxDelayMs = 10000; // 最大延迟时间(毫秒)
private double backoffMultiplier = 2.0; // 退避乘数
private boolean exponentialBackoff = true; // 是否使用指数退避
private Set<Class> retryableExceptions = new HashSet<>(); // 可重试的异常
private RetryLogger logger = null; // 重试日志记录器
// Builder模式构建器
public static class Builder {
private RetryConfig config = new RetryConfig();
public Builder maxAttempts(int maxAttempts) {
config.maxAttempts = maxAttempts;
return this;
}
public Builder initialDelay(long delayMs) {
config.initialDelayMs = delayMs;
return this;
}
public Builder maxDelay(long maxDelayMs) {
config.maxDelayMs = maxDelayMs;
return this;
}
public Builder backoffMultiplier(double multiplier) {
config.backoffMultiplier = multiplier;
return this;
}
public Builder fixedDelay() {
config.exponentialBackoff = false;
return this;
}
public Builder exponentialBackoff() {
config.exponentialBackoff = true;
return this;
}
public Builder retryOn(Class... exceptions) {
Collections.addAll(config.retryableExceptions, exceptions);
return this;
}
public Builder logger(RetryLogger logger) {
config.logger = logger;
return this;
}
public RetryConfig build() {
// 如果没有指定可重试异常,默认重试所有异常
if (config.retryableExceptions.isEmpty()) {
config.retryableExceptions.add(Exception.class);
}
// 如果没有指定日志记录器,使用默认日志记录器
if (config.logger == null) {
config.logger = new DefaultRetryLogger();
}
return config;
}
}
public static Builder builder() {
return new Builder();
}
}
/**
* 重试日志记录器接口
*/
public interface RetryLogger {
void logRetryAttempt(int attempt, long delay, Exception exception);
void logSuccess(int attempts);
void logFailure(int attempts, Exception lastException);
}
/**
* 默认日志记录器实现
*/
public static class DefaultRetryLogger implements RetryLogger {
@Override
public void logRetryAttempt(int attempt, long delay, Exception exception) {
System.out.printf("[重试框架] 第%d次尝试失败,将在%dms后重试,异常:%s: %s%n",
attempt, delay, exception.getClass().getSimpleName(), exception.getMessage());
}
@Override
public void logSuccess(int attempts) {
if (attempts > 1) {
System.out.printf("[重试框架] 操作在第%d次尝试后成功%n", attempts);
}
}
@Override
public void logFailure(int attempts, Exception lastException) {
System.err.printf("[重试框架] 操作在尝试%d次后最终失败,最后异常:%s: %s%n",
attempts, lastException.getClass().getSimpleName(), lastException.getMessage());
}
}
/**
* 执行需要重试的操作
*
* @param operation 需要执行的操作
* @param config 重试配置
* @param 操作返回值类型
* @return 操作执行结果
* @throws Exception 如果重试后仍然失败
*/
public static T executeWithRetry(Supplier operation, RetryConfig config) throws Exception {
int attempts = 0;
long delay = config.initialDelayMs;
Exception lastException = null;
while (attempts < config.maxattempts attempts try t result='operation.get();' config.logger.logsuccessattempts return result catch exception e lastexception='e;' boolean isretryable='config.retryableExceptions.stream()' .anymatchexclass -> exClass.isAssignableFrom(e.getClass()));
// 如果是不可重试异常或已达到最大尝试次数,则直接抛出
if (!isRetryable || attempts >= config.maxAttempts) {
config.logger.logFailure(attempts, e);
throw e;
}
// 记录重试日志
config.logger.logRetryAttempt(attempts, delay, e);
// 等待指定时间后重试
try {
Thread.sleep(delay);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试过程被中断", ie);
}
// 计算下一次重试的延迟时间
if (config.exponentialBackoff) {
delay = Math.min((long)(delay * config.backoffMultiplier), config.maxDelayMs);
}
}
}
// 这里通常不会执行到,因为最后一次失败会直接抛出异常
throw new RuntimeException("达到最大重试次数后仍然失败", lastException);
}
/**
* 使用示例
*/
public static void main(String[] args) {
try {
// 构建重试配置
RetryConfig config = RetryConfig.builder()
.maxAttempts(5)
.initialDelay(500)
.maxDelay(8000)
.exponentialBackoff()
.retryOn(IOException.class, TimeoutException.class)
.build();
// 执行需要重试的操作
String result = executeWithRetry(() -> {
// 模拟外部API调用
System.out.println("调用外部API...");
if (Math.random() < 0.8) { // 80%的概率失败
if (Math.random() < 0.5) {
throw new IOException("网络连接异常");
} else {
throw new TimeoutException("请求超时");
}
}
return "API调用成功返回的数据";
}, config);
System.out.println("最终结果: " + result);
} catch (Exception e) {
System.err.println("所有重试尝试均失败: " + e.getMessage());
}
}
}
// 针对HTTP调用的重试配置
RetryConfig httpConfig = RetryConfig.builder()
.maxAttempts(3)
.initialDelay(1000)
.exponentialBackoff()
.retryOn(IOException.class, SocketTimeoutException.class)
.logger(new CustomHttpRetryLogger())
.build();
// 针对数据库操作的重试配置
RetryConfig dbConfig = RetryConfig.builder()
.maxAttempts(5)
.initialDelay(200)
.fixedDelay()
.retryOn(SQLTransientException.class)
.build();
总结
最后,重试虽好,但也不是万能的。有时候,优化接口本身的性能和稳定性,可能比添加复杂的重试逻辑更加重要。在系统设计中,我们应该始终保持平衡的视角,选择最适合当前场景的解决方案。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至2705686032@qq.com 举报,一经查实,本站将立刻删除。原文转载: 原文出处: