Java 重试机制详解
文章目录
- 1. 重试机制基础
- 1.1 什么是重试机制
- 1.2 重试机制的关键要素
- 1.3 适合重试的场景
- 2. 基础重试实现
- 2.1 简单循环重试
- 2.2 带延迟的重试
- 2.3 指数退避策略
- 2.4 添加随机抖动
- 2.5 使用递归实现重试
- 2.6 可重试异常过滤
- 3. 常用重试库介绍
- 3.1 Spring Retry
- 3.1.1 依赖配置
- 3.1.2 编程式重试
- 3.1.3 声明式重试
- 3.2 Resilience4j
- 3.2.1 依赖配置
- 3.2.2 基本使用
- 3.2.3 高级配置
- 3.3 Guava Retrying
- 3.3.1 依赖配置
- 3.3.2 基本使用
- 3.3.3 高级配置
- 3.4 Apache Commons Lang - 轻量级重试工具
- 3.4.1 依赖配置
- 3.4.2 基本使用
- 4. 实际应用场景
- 4.1 HTTP 请求重试
- 4.1.1 使用 OkHttp 实现 HTTP 请求重试
- 4.1.2 使用 Spring RestTemplate 重试
- 4.2 数据库操作重试
- 4.3 消息队列消费重试
- 4.4 分布式锁获取重试
- 5. 重试机制最佳实践
- 5.1 设计原则
- 5.2 避免的陷阱
- 5.3 性能考虑
- 5.4 配置建议
- 5.5 异步重试和重试队列
- 6. 总结
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());}}// 定义一个函数式接口,表示可能抛出异常的操作@FunctionalInterfaceinterface 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依赖 -->