1. 重试机制基础
1.1 什么是重试机制
重试机制是一种容错设计模式,在分布式系统和网络通信中尤为重要。当操作失败时(例如网络请求超时、数据库连接失败等),系统会自动重新尝试执行该操作,直到成功或达到预定的重试次数上限。
在Java应用程序中,特别是涉及外部服务调用、数据库操作或文件IO等不可靠操作时,合理的重试机制可以:
- 提高系统的可用性和稳定性
- 处理瞬时故障,避免级联失败
- 减少人工干预的需要
- 优化用户体验
1.2 重试机制的关键要素
一个完善的重试机制通常包含以下几个关键要素:
- 重试触发条件:何种异常或错误状态下需要进行重试
- 重试次数:最大允许重试的次数
- 重试间隔:两次重试之间的时间间隔
- 退避策略:重试间隔如何变化(固定、递增、指数等)
- 超时机制:整个重试过程的最长允许时间
- 恢复策略:重试全部失败后的处理方式
- 重试结果处理:成功或失败的回调处理
1.3 适合重试的场景
并非所有失败的操作都适合重试。一般来说,以下场景适合实施重试机制:
- 幂等操作:重复执行不会产生副作用的操作(如GET请求、查询操作)
- 瞬时故障:可能自行恢复的短暂故障(如网络抖动、服务器临时过载)
- 资源竞争:因资源暂时不可用导致的失败(如数据库死锁、连接池耗尽)
不适合重试的场景:
- 非幂等操作(如未做好幂等性保障的支付操作)
- 由于请求参数错误导致的失败
- 因权限不足导致的失败
- 业务逻辑错误
2. 基础重试实现
2.1 简单循环重试
最基本的重试机制是使用循环来实现:
public class SimpleRetry {
public static void main(String[] args) {
int maxRetries = 3;
int retryCount = 0;
boolean success = false;
while (!success && retryCount < maxRetries) {
try {
// 执行可能失败的操作
doSomethingRisky();
success = true;
System.out.println("操作成功!");
} catch (Exception e) {
retryCount++;
System.out.println("操作失败,这是第 " + retryCount + " 次重试");
if (retryCount >= maxRetries) {
System.out.println("重试次数已达上限,操作最终失败");
}
}
}
}
private static void doSomethingRisky() throws Exception {
// 模拟一个有75%几率失败的操作
if (Math.random() < 0.75) {
throw new Exception("操作失败,需要重试");
}
}
}
2.2 带延迟的重试
实际应用中,通常需要在重试之间添加一定的延迟,避免立即重试导致的资源浪费:
public class DelayedRetry {
public static void main(String[] args) {
int maxRetries = 3;
int retryCount = 0;
boolean success = false;
long retryDelayMillis = 1000; // 1秒延迟
while (!success && retryCount < maxRetries) {
try {
doSomethingRisky();
success = true;
System.out.println("操作成功!");
} catch (Exception e) {
retryCount++;
System.out.println("操作失败,这是第 " + retryCount + " 次重试");
if (retryCount >= maxRetries) {
System.out.println("重试次数已达上限,操作最终失败");
} else {
try {
System.out.println("等待 " + retryDelayMillis + " 毫秒后重试...");
Thread.sleep(retryDelayMillis);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
System.out.println("重试过程被中断");
break;
}
}
}
}
}
private static void doSomethingRisky() throws Exception {
// 模拟一个有75%几率失败的操作
if (Math.random() < 0.75) {
throw new Exception("操作失败,需要重试");
}
}
}
2.3 指数退避策略
在网络请求等场景中,通常使用指数退避策略,即每次重试的等待时间呈指数增长:
public class ExponentialBackoffRetry {
public static void main(String[] args) {
int maxRetries = 5;
int retryCount = 0;
boolean success = false;
long initialDelayMillis = 1000; // 初始延迟1秒
while (!success && retryCount < maxRetries) {
try {
doSomethingRisky();
success = true;
System.out.println("操作成功!");
} catch (Exception e) {
retryCount++;
System.out.println("操作失败,这是第 " + retryCount + " 次重试");
if (retryCount >= maxRetries) {
System.out.println("重试次数已达上限,操作最终失败");
} else {
// 计算指数退避延迟时间:初始延迟 * (2^重试次数)
long delayMillis = initialDelayMillis * (long) Math.pow(2, retryCount - 1);
try {
System.out.println("等待 " + delayMillis + " 毫秒后重试...");
Thread.sleep(delayMillis);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
System.out.println("重试过程被中断");
break;
}
}
}
}
}
private static void doSomethingRisky() throws Exception {
// 模拟一个有75%几率失败的操作
if (Math.random() < 0.75) {
throw new Exception("操作失败,需要重试");
}
}
}
2.4 添加随机抖动
在高并发环境中,为了避免大量请求同时重试导致的"惊群效应",通常会给退避时间添加一些随机抖动:
public class JitteredBackoffRetry {
public static void main(String[] args) {
int maxRetries = 5;
int retryCount = 0;
boolean success = false;
long initialDelayMillis = 1000; // 初始延迟1秒
double jitterFactor = 0.5; // 抖动因子
while (!success && retryCount < maxRetries) {
try {
doSomethingRisky();
success = true;
System.out.println("操作成功!");
} catch (Exception e) {
retryCount++;
System.out.println("操作失败,这是第 " + retryCount + " 次重试");
if (retryCount >= maxRetries) {
System.out.println("重试次数已达上限,操作最终失败");
} else {
// 计算指数退避延迟时间
long baseDelayMillis = initialDelayMillis * (long) Math.pow(2, retryCount - 1);
// 添加随机抖动:基础延迟 ± (基础延迟 * 抖动因子 * 随机值)
long jitter = (long) (baseDelayMillis * jitterFactor * Math.random());
// 随机决定是加还是减
long delayMillis = Math.random() > 0.5 ?
baseDelayMillis + jitter :
Math.max(0, baseDelayMillis - jitter);
try {
System.out.println("等待 " + delayMillis + " 毫秒后重试...");
Thread.sleep(delayMillis);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
System.out.println("重试过程被中断");
break;
}
}
}
}
}
private static void doSomethingRisky() throws Exception {
// 模拟一个有75%几率失败的操作
if (Math.random() < 0.75) {
throw new Exception("操作失败,需要重试");
}
}
}
2.5 使用递归实现重试
除了循环,也可以使用递归来实现重试逻辑:
public class RecursiveRetry {
public static void main(String[] args) {
try {
String result = executeWithRetry(RecursiveRetry::doSomethingRisky, 3, 1000);
System.out.println("最终结果: " + result);
} catch (Exception e) {
System.out.println("操作最终失败: " + e.getMessage());
}
}
// 定义一个函数式接口,表示可能抛出异常的操作
@FunctionalInterface
interface RiskyOperation<T> {
T execute() throws Exception;
}
// 递归重试方法
private static <T> T executeWithRetry(RiskyOperation<T> operation,
int maxRetries,
long delayMillis) throws Exception {
try {
return operation.execute();
} catch (Exception e) {
if (maxRetries > 0) {
System.out.println("操作失败,剩余重试次数: " + maxRetries);
System.out.println("等待 " + delayMillis + " 毫秒后重试...");
try {
Thread.sleep(delayMillis);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
throw new RuntimeException("重试过程被中断", ie);
}
// 递归调用,次数减1,延迟翻倍(指数退避)
return executeWithRetry(operation, maxRetries - 1, delayMillis * 2);
} else {
throw new Exception("重试次数已用尽,操作失败: " + e.getMessage(), e);
}
}
}
// 模拟一个可能失败的操作
private static String doSomethingRisky() throws Exception {
if (Math.random() < 0.75) {
throw new Exception("操作失败,需要重试");
}
return "操作成功";
}
}
2.6 可重试异常过滤
在实际应用中,我们通常只对特定类型的异常进行重试,对于其他异常则直接失败:
public class SelectiveRetry {
public static void main(String[] args) {
int maxRetries = 3;
int retryCount = 0;
boolean success = false;
while (!success && retryCount < maxRetries) {
try {
doSomethingRisky();
success = true;
System.out.println("操作成功!");
} catch (Exception e) {
// 只对特定异常进行重试
if (isRetryable(e)) {
retryCount++;
System.out.println("发生可重试异常: " + e.getMessage());
System.out.println("这是第 " + retryCount + " 次重试");
if (retryCount >= maxRetries) {
System.out.println("重试次数已达上限,操作最终失败");
} else {
try {
Thread.sleep(1000);
} catch (InterruptedException ie) {
Thread.currentThread().interrupt();
break;
}
}
} else {
// 对于不可重试的异常,直接失败退出
System.out.println("发生不可重试异常: " + e.getMessage());
System.out.println("立即退出重试");
break;
}
}
}
}
// 判断异常是否可重试
private static boolean isRetryable(Exception e) {
// 例如,只对超时、连接和IO异常进行重试
return e instanceof java.net.SocketTimeoutException ||
e instanceof java.net.ConnectException ||
e instanceof java.io.IOException;
}
private static void doSomethingRisky() throws Exception {
double random = Math.random();
if (random < 0.4) {
// 模拟一个可重试的异常
throw new java.net.SocketTimeoutException("网络超时");
} else if (random < 0.7) {
// 模拟一个不可重试的异常
throw new IllegalArgumentException("参数错误");
}
// 成功
}
}
3. 常用重试库介绍
除了手动实现重试逻辑,Java生态系统中有许多成熟的重试库,提供了更加灵活和强大的重试机制。下面介绍几个常用的重试库:
3.1 Spring Retry
Spring Retry 是 Spring 生态系统中的一个重试库,提供了声明式重试和编程式重试两种方式。
3.1.1 依赖配置
<!-- Maven依赖 -->
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.3.4</version>
</dependency>
<!-- 如果使用声明式重试,需要添加AOP依赖 -->