引言
最近需要对接很多第三方接口,有些接口因数据量大导致超时,有些因网络不稳定导致接口异常。在开发阶段还可以看日志、debugger处理,生产环境则需要更完善的方式处理。
重试的基本原则
- 在实现重试机制前,我们需要明确几个基本原则:
1.不是所有错误都适合重试:例如参数错误、认证失败等客户端错误通常不适合重试
2.重试间隔应该递增:避免对目标系统造成雪崩效应
3.设置最大重试次数:防止无限重试导致资源耗尽
4.考虑幂等性:确保重试不会导致业务逻辑重复执行
基础实现:自定义重试逻辑
/**
* 基础重试工具类
*/
public class RetryUtil {
/**
* 执行带重试的操作
* @param operation 需要执行的操作
* @param maxAttempts 最大尝试次数
* @param initialDelayMs 初始延迟时间(毫秒)
* @param maxDelayMs 最大延迟时间(毫秒)
* @param retryableExceptions 需要重试的异常类
* @param <T> 返回值类型
* @return 操作执行的结果
* @throws Exception 如果重试后仍然失败
*/
public static <T> T executeWithRetry(
Supplier<T> operation,
int maxAttempts,
long initialDelayMs,
long maxDelayMs,
Set<Class<? extends Exception>> 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()
.anyMatch(exClass -> 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<? extends Exception>> 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返回数据";
}
}
- 上面的代码实现了一个基础的重试机制,具有以下特点:
1.支持设置最大重试次数
2.使用指数退避算法增加重试间隔
3.可以指定哪些异常类型需要重试
4.透明地处理重试逻辑,对调用方几乎无感知
通用重试框架:灵活且强大的解决方案
- 为了应对各种复杂场景,我们可能需要更加灵活的重试框架。以下代码实现了一个通用重试框架,它具有以下特点:
1.Builder模式:使用Builder模式构建重试配置,使API更加直观
2.灵活的策略配置:支持配置最大尝试次数、初始延迟、最大延迟、退避乘数等
3.多种退避策略:支持固定延迟和指数退避两种策略
4.可定制的异常处理:可以精确控制哪些异常需要重试
5.日志记录:通过接口抽象,支持可定制的日志记录方式
6.链式调用: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<? extends Exception>> 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<? extends Exception>... 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 <T> 操作返回值类型
* @return 操作执行结果
* @throws Exception 如果重试后仍然失败
*/
public static <T> T executeWithRetry(Supplier<T> 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.logSuccess(attempts);
return result;
} catch (Exception e) {
lastException = e;
// 判断异常是否可重试
boolean isRetryable = config.retryableExceptions.stream()
.anyMatch(exClass -> 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();
总结
重试虽好,但也不是万能的。有时候,优化接口本身的性能和稳定性,可能比添加复杂的重试逻辑更加重要。在系统设计中,我们应该始终保持平衡的视角,选择最适合当前场景的解决方案。