当前位置: 首页 > java >正文

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. 重试触发条件:何种异常或错误状态下需要进行重试
  2. 重试次数:最大允许重试的次数
  3. 重试间隔:两次重试之间的时间间隔
  4. 退避策略:重试间隔如何变化(固定、递增、指数等)
  5. 超时机制:整个重试过程的最长允许时间
  6. 恢复策略:重试全部失败后的处理方式
  7. 重试结果处理:成功或失败的回调处理

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依赖 -->
http://www.xdnf.cn/news/6063.html

相关文章:

  • QT之QComboBox组件
  • 软考 系统架构设计师系列知识点之杂项集萃(59)
  • 【springcloud学习(dalston.sr1)】Eureka单个服务端的搭建(含源代码)(三)
  • Python 常用模块(八):logging模块
  • 基于GpuGeek平台的深度学习项目
  • Keil5 MDK 安装教程
  • LeetCode 热题 100 35.搜索插入位置
  • python打包exe报错:处理文件时错误:Excel xlsx file; not supported
  • iOS Safari调试教程
  • vue使用路由技术实现登录成功后跳转到首页
  • 【Vue 3 + Vue Router 4】如何正确重置路由实例(resetRouter)——避免“VueRouter is not defined”错误
  • 数据结构与算法:状压dp
  • 反向传播算法——矩阵形式递推公式——ReLU传递函数
  • 如何保证RabbitMQ消息的顺序性?
  • 简单易懂的JavaScript中的this指针
  • 现代计算机图形学Games101入门笔记(三)
  • Node.js中MongoDB连接的进阶模块化封装
  • hadoop中spark基本介绍
  • 从零构建知识图谱:使用大语言模型处理复杂数据的11步实践指南
  • 【C语言指针超详解(六)】--sizeof和strlen的对比,数组和指针笔试题解析,指针运算笔试题解析
  • LIO-SAM框架理解
  • ECharts:数据可视化的强大引擎
  • MySQL增删查改进阶
  • 小程序 存存上下滑动的页面
  • SQL看最多的数据,但想从小到大排列看趋势
  • 使用大模型预测急性结石性疾病技术方案
  • 进阶数据结构: AVL树
  • Linux复习笔记(五) 网络服务配置(dhcp)
  • CPS联盟+小程序聚合平台分销返利系统开发|小红书番茄网盘CPA拉新推广全解析
  • Golang实践录:在go中使用curl实现https请求