深入了解Spring重试组件spring-retry

news2024/11/25 18:52:18

在我们的项目中,为了提高程序的健壮性,很多时候都需要有重试机制进行兜底,最多就场景就比如调用远程的服务,调用中间件服务等,因为网络是不稳定的,所以在进行远程调用的时候偶尔会产生超时的异常,所以一般来说我们都会通过手动去写一些重试的代码去进行兜底,而这些重试的代码其实都是模板化的,因此Spring实现了自己的重试机制组件spring-retry,下面我们就一起来学习一下spring-retry这个组件吧

使用方式

1.编程式

// 创建一个RetryTemplate
RetryTemplate retryTemplate = RetryTemplate.builder()
    .customPolicy(new SimpleRetryPolicy()) // 指定重试策略,默认重试3次
    .exponentialBackoff(1000L, 2, 10000L) // 指定重试的退避时间策略
    .withListener(new RetryListenerDemo())// 重试监听器
    .build();

// 通过RetryTemplate的execute方法执行业务逻辑
retryTemplate.execute(retryContext -> {
    log.info("开始执行");
    throw new RuntimeException("抛出异常");
}, context -> recoverMethod());
// 当重试结束还是失败之后最后兜底执行的方法
public String recoverMethod() {
    log.info("执行恢复");
    return "执行了Recover方法";
}
public class RetryListenerDemo implements RetryListener {
    @Override
    public <T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback) {
        log.info("{}", context.getRetryCount());
        log.info("listener>>>开始监听");
        //        return false; // 否决整个重试
        return true; // 继续重试
    }

    @Override
    public <T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("listener>>>关闭");
    }

    @Override
    public <T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> callback, Throwable throwable) {
        log.info("listener>>>报错了");
    }
}

这里说一下重试监听器,自定义的重试监听器需要实现RetryListener接口,该接口主要包括三个方法:

  • open:在执行我们的业务逻辑之前会执行一次open方法,如果该方法返回false,则会直接抛出一个TerminatedRetryException异常,从而不会往下执行业务逻辑,返回true则正常往下执行
  • close:当重试结束之后,或者open方法返回false的时候就会触发close方法
  • onError:在每一次业务逻辑抛出异常的时候都会执行onError方法

2.声明式

@Retryable(value = Exception.class, maxAttempts = 3, listeners = {"retryListenerDemo"})
public String test() {
    log.info("开始执行");
    throw new RuntimeException("抛出异常");
}
@Recover
public String recoverMethod() {
    log.info("执行恢复");
    return "执行了Recover方法";
}

声明式只需要在需要重试的方法上加上Retryable注解,并且在注解上指定一些重试的属性,比如重试次数,触发重试的异常,重试监听器等等,这些属性对应在编程式中都能进行设置。而对于重试兜底方法则需要Recover注解进行标识

重试策略RetryPolicy

在对重试属性进行配置的时候我们可以去配置不同的重试策略,所谓的重试策略,其实就是去判断是否能够进行重试,也就是RetryPolicy,它是一个接口

public interface RetryPolicy extends Serializable {

	/**
	 * 是否能够重试
	 *
	 * @param context 重试上下文
	 * @return true=>允许重试,false=>不允许重试
	 */
	boolean canRetry(RetryContext context);

	/**
	 * 获取一个重试上下文,不同的重试策略有自己的重试上下文
	 *
	 * @param parent 父级重试上下文
	 * @return a {@link RetryContext} object specific to this policy.
	 *
	 */
	RetryContext open(RetryContext parent);

	/**
	 * 关闭这个重试上下文
	 */
	void close(RetryContext context);

	/**
	 * 每一次重试失败后会回调该方法,然后通过重试上下文记录下重试的异常,方便在下一次canRetry方法中从重试上下文中去判断是否还能进行重试
	 * @param context 重试上下文
	 * @param throwable 重试时抛出的异常
	 */
	void registerThrowable(RetryContext context, Throwable throwable);

}

该接口在spring-retry中提供多种不同的重试策略的实现

  • SimpleRetryPolicy:这是一种简单的重试策略,允许根据最大重试次数和特定的异常列表来控制重试行为
  • NeverRetryPolicy:不进行重试的重试策略,也就是说我们的业务逻辑代码在第一次执行如果抛出异常了,不会进行重试
  • AlwaysRetryPolicy:允许一直重试的重试策略
  • TimeoutRetryPolicy:通过设置重试的时间段,仅允许在未超过该时间段的时候才进行重试
  • CompositeRetryPolicy:组合重试策略,可以组合多种重试策略,这对于需要复杂条件的情况非常有用
  • ExpressionRetryPolicy:该策略继承了SimpleRetryPolicy,在SimpleRetryPolicy的基础上加上了基于spel表达式去判断是否需要进行重试的功能

在RetryPolicy接口中关键的方法就是canRetry,canRetry方法会在重试之前进行调用,用来判断是否还能够继续进行重试,而判断所需的一些上下文属性(例如已经重试的次数,重试的超时时间)就在重试上下文RetryContext中,对于每一种重试策略来说,都会有自己的RetryContext,因为不同的重试策略它们用来判断重试机会的时候所需的上下文属性是不一样的

以TimeoutRetryPolicy为例,它具有限制重试时间的功能,那自然就需要记录下重试的起始时间和重试的超时时间了,而这两个信息就会放在TimeoutRetryContext中

private static class TimeoutRetryContext extends RetryContextSupport {

    /**
    * 允许重试的时间段
    */
    private long timeout;

    /**
    * 重试开始时间
    */
    private long start;

    public TimeoutRetryContext(RetryContext parent, long timeout) {
        super(parent);
        this.start = System.currentTimeMillis();
        this.timeout = timeout;
    }

    /**
    * 判断当前是否超过了重试时间
    * @return true=>允许重试,false=>已经超过了重试时间了,不允许重试
    */
    public boolean isAlive() {
        return (System.currentTimeMillis() - start) <= timeout;
    }

}

这样就可以在下一次判断是否能够重试的时候,也就是调用canRetry方法的时候通过传入TimeoutRetryContext去判断重试是否超时了

退避策略BackOffPolicy

上面说的RetryPolicy主要是在每一次要重试之前用来判断是否能够进行重试的,而BackOffPolicy则是提供了重试之间的间隔时间多久的功能,也就是说会先去执行RetryPolicy判断是否允许重试,如果允许重试,则才会去执行BackOffPolicy去等待重试的执行

public interface BackOffPolicy {

    /**
    * 创建一个退避上下文
    *
    * @param context the {@link RetryContext} context, which might contain information
    * that we can use to decide how to proceed.
    * @return the implementation-specific {@link BackOffContext} or '<code>null</code>'.
    */
    BackOffContext start(RetryContext context);

    /**
    * 执行退避操作
    * @param backOffContext the {@link BackOffContext}
    * @throws BackOffInterruptedException if the attempt at back off is interrupted.
    */
    void backOff(BackOffContext backOffContext) throws BackOffInterruptedException;

}

spring-retry也提供了不同的BackOffPolicy实现

  • NoBackOffPolicy:一个不执行任何操作的 BackOffPolicy,即不会增加等待时间。适用于不需要等待时间间隔的情况
  • FixedBackOffPolicy:以固定时间去进行重试退避
  • ExponentialBackOffPolicy:退避时间以指数形式增长

执行流程

protected <T, E extends Throwable> T doExecute(RetryCallback<T, E> retryCallback,
                                               RecoveryCallback<T> recoveryCallback, RetryState state) throws E, ExhaustedRetryException {

    RetryPolicy retryPolicy = this.retryPolicy;
    BackOffPolicy backOffPolicy = this.backOffPolicy;

    // 获取当前的重试上下文
    RetryContext context = open(retryPolicy, state);
    if (this.logger.isTraceEnabled()) {
        this.logger.trace("RetryContext retrieved: " + context);
    }

    // 把当前的重试上下文设置到ThreadLocal中
    RetrySynchronizationManager.register(context);

    Throwable lastException = null;

    boolean exhausted = false;
    try {
        // 遍历所有的重试监听器,执行其open方法
        boolean running = doOpenInterceptors(retryCallback, context);

        // 条件成立:有其中一个重试监听器的open方法返回了false
        if (!running) {
            // 抛出异常
            throw new TerminatedRetryException("Retry terminated abnormally by interceptor before first attempt");
        }

        // Get or Start the backoff context...
        BackOffContext backOffContext = null;
        // 尝试从当前的重试上下文中获取退避上下文
        Object resource = context.getAttribute("backOffContext");

        if (resource instanceof BackOffContext) {
            backOffContext = (BackOffContext) resource;
        }

        // 条件成立:说明当前的重试上下文中没有设置退避上下文
        if (backOffContext == null) {
            // 这时候通过退避策略创建出对应的退避上下文
            backOffContext = backOffPolicy.start(context);
            // 再把这个退避上下文放到重试上下文中
            if (backOffContext != null) {
                context.setAttribute("backOffContext", backOffContext);
            }
        }

        // 条件成立:当前配置的重试策略允许重试,并且当前的重试上下文中没有设置中断重试的标志
        while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {

            try {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Retry: count=" + context.getRetryCount());
                }
                // Reset the last exception, so if we are successful
                // the close interceptors will not think we failed...
                lastException = null;
                // 执行retryCallback,也就是执行目标重试方法
                return retryCallback.doWithRetry(context);
            }
                // 执行目标重试方法时抛异常了
            catch (Throwable e) {

                lastException = e;

                try {
                    // 此时在重试上下文中记录下重试异常
                    registerThrowable(retryPolicy, state, context, e);
                }
                catch (Exception ex) {
                    throw new TerminatedRetryException("Could not register throwable", ex);
                }
                finally {
                    // 遍历所有的重试监听器,执行其onError方法
                    doOnErrorInterceptors(retryCallback, context, e);
                }

                // 在执行退避策略之前再判断一下是否还能重试
                if (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
               try {
                  // 执行退避策略
                  backOffPolicy.backOff(backOffContext);
               }
               catch (BackOffInterruptedException ex) {
                  lastException = e;
                  // back off was prevented by another thread - fail the retry
                  if (this.logger.isDebugEnabled()) {
                     this.logger.debug("Abort retry because interrupted: count=" + context.getRetryCount());
                  }
                  throw ex;
               }
            }

            if (this.logger.isDebugEnabled()) {
               this.logger.debug("Checking for rethrow: count=" + context.getRetryCount());
            }

            if (shouldRethrow(retryPolicy, context, state)) {
               if (this.logger.isDebugEnabled()) {
                  this.logger.debug("Rethrow in retry for policy: count=" + context.getRetryCount());
               }
               throw RetryTemplate.<E>wrapIfNecessary(e);
            }

         }

         /*
          * A stateful attempt that can retry may rethrow the exception before now,
          * but if we get this far in a stateful retry there's a reason for it,
          * like a circuit breaker or a rollback classifier.
          */
         if (state != null && context.hasAttribute(GLOBAL_STATE)) {
            break;
         }
      }

      // 代码执行到这里说明重试结束了
      if (state == null && this.logger.isDebugEnabled()) {
         this.logger.debug("Retry failed last attempt: count=" + context.getRetryCount());
      }

      exhausted = true;
      // 重试结束之后,最后执行recover方法,并返回recover方法的执行结果
      return handleRetryExhausted(recoveryCallback, context, state);

   }
   // 上面try中抛出异常之后catch
   catch (Throwable e) {
      throw RetryTemplate.<E>wrapIfNecessary(e);
   }
   finally {
      close(retryPolicy, context, state, lastException == null || exhausted);
      // 执行所有重试监听器的close方法
      doCloseInterceptors(retryCallback, context, lastException);
      // 在ThreadLocal中清除当前的重试上下文,如有必要,还需把父级上下文设置回ThreadLocal中
      RetrySynchronizationManager.clear();
   }

}

上面就是执行重试的核心流程代码,注释都详细写上去了,就不多说了。这里有个说一下的就是如果存在嵌套重试的话,我们需要去保存父层级的RetryContext,什么叫嵌套重试?就是在一个重试方法中调用了另一个重试方法,这两个重试方法的重试规则可能都不一样,这时候在执行第二个重试方法的时候就需要把第一个重试方法的RetryContext进行保存,那spring-retry是怎么保存的呢?在RetryContext中会有一个parent,这个parent记录的就是当前上一层的RetryContext,而当第二层重试执行完之后,这时候就会返回上一层的重试,所以就需要把上一层的RetryContext复原,这个复原的动作会在上面最后的finally代码块中执行。关联父子RetryContext的操作会在RetryPolicy的open方法中去执行,传入的参数就是父级的RetryContext

	/**
	 * 获取一个重试上下文,不同的重试策略有自己的重试上下文
	 *
	 * @param parent 父级重试上下文
	 * @return a {@link RetryContext} object specific to this policy.
	 *
	 */
	RetryContext open(RetryContext parent);

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2221258.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

渗透测试实战—教育攻防演练中突破网络隔离

免责声明&#xff1a;文章来源于真实渗透测试&#xff0c;已获得授权&#xff0c;且关键信息已经打码处理&#xff0c;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失&#xff0c;均由使用者本…

3.matplotlib基础及用法(全)

一.基础绘图 折线图plot散点图scatter柱状图bar饼图pie 二.图表设置 设置标题设置线条设置坐标轴添加图例添加注释设置画布大小与分辨率 三.高级功能 绘制子图保存图形 一.基础绘图 1.折线图plot import matplotlib.pyplot as plt x [1, 2, 3, 4, 5] y [2, 3, 5, 7, 11] pl…

如何选择合适的电感器来匹配感性负载?

在匹配感性负载时&#xff0c;选择合适的电感器是至关重要的。电感器的主要作用是抑制电流变化&#xff0c;从而维持电路的稳定性。为了确保电路的稳定运行&#xff0c;需要考虑以下因素&#xff1a; 1. 电流和电压&#xff1a;首先&#xff0c;需要确定电感器的额定电流和额定…

GJS-WCP

不懂的就问&#xff0c;但我也是二把手......哭死 web GJS-ezssti 很常规的ssti模板注入&#xff0c;只过滤了"/","flag"。 过滤了/,flag 可以利用bash的特性绕过&#xff0c;如字符串截取&#xff0c;环境变量等等。payload1: {{url_for.__globals__[…

【uniapp】微信小程序使用echarts图表记录

1、插件引入 在Dcloud插件市场下载echarts插件&#xff1a;插件地址 或去相关代码库下载js&#xff1a;gitee地址 将static文件夹下中的echarts.min.js和ecStat.min.js复制到自己项目的static文件夹内或到echarts官方定制自己需要的图表类型下载js文件并放入相关目录。echart…

让你的 IDEA 使用更流畅 | IDEA内存修改

随着idea使用越来越频繁&#xff0c;笔者最近发现使用过程中有时候会出现卡顿现象&#xff0c;例如&#xff0c;启动软件变慢&#xff0c;打开项目的速度变慢等&#xff1a; 因此如果各位朋友觉得最近也遇到了同样的困惑&#xff0c;不妨跟着笔者一起来设置IDEA的内存大小吧~ …

【C#】在 WinForms 中使用 MVVM(Model-View-ViewModel) 设计模式

结合当前的 DevExpress 项目&#xff0c;在 WinForms 中使用 MVVM&#xff08;Model-View-ViewModel&#xff09; 设计模式。这个例子将通过数据绑定、命令绑定来展示 MVVM 模式的运用。 1. 项目结构 假设我们要实现一个简单的应用程序&#xff0c;它有一个文本框和一个按钮&…

【C++指南】类和对象(四):类的默认成员函数——全面剖析 : 拷贝构造函数

引言 拷贝构造函数是C中一个重要的特性&#xff0c;它允许一个对象通过另一个已创建好的同类型对象来初始化。 了解拷贝构造函数的概念、作用、特点、规则、默认行为以及如何自定义实现&#xff0c;对于编写健壮和高效的C程序至关重要。 C类和对象系列文章&#xff0c;可点击下…

【计网】理解TCP全连接队列与tcpdump抓包

希望是火&#xff0c;失望是烟&#xff0c; 生活就是一边点火&#xff0c;一边冒烟。 理解TCP全连接队列与tcpdump抓包 1 TCP 全连接队列1.1 重谈listen函数1.2 初步理解全连接队列1.3 深入理解全连接队列 2 tcpdump抓包 1 TCP 全连接队列 1.1 重谈listen函数 这里我们使用…

【JAVA】第三张_Eclipse下载、安装、汉化

简介 Eclipse是一种流行的集成开发环境&#xff08;IDE&#xff09;&#xff0c;可用于开发各种编程语言&#xff0c;包括Java、C、Python等。它最初由IBM公司开发&#xff0c;后来被Eclipse Foundation接手并成为一个开源项目。 Eclipse提供了一个功能强大的开发平台&#x…

IDEA如何查看所有的断点(Breakpoints)并关闭

前言 我们在使用IDEA开发Java应用时&#xff0c;基本上都需要进行打断点的操作&#xff0c;这方便我们排查BUG&#xff0c;也方便我们查看设计的是否正确。 不过有时候&#xff0c;我们不希望进入断点&#xff0c;这时候除了点击断点关闭外&#xff0c;有没有更快速的方便关闭…

深度解析机器学习的四大核心功能:分类、回归、聚类与降维

深度解析机器学习的四大核心功能&#xff1a;分类、回归、聚类与降维 前言分类&#xff08;Classification&#xff09;&#xff1a;预测离散标签的艺术关键算法与代码示例逻辑回归支持向量机&#xff08;SVM&#xff09; 回归&#xff08;Regression&#xff09;&#xff1a;预…

HarmonyOS Next应用开发——图像PixelMap变换

【高心星出品】 图像变换 图片处理指对PixelMap进行相关的操作&#xff0c;如获取图片信息、裁剪、缩放、偏移、旋转、翻转、设置透明度、读写像素数据等。图片处理主要包括图像变换、位图操作&#xff0c;本文介绍图像变换。 图形裁剪 // 裁剪图片 x&#xff0c;y为裁剪的起…

impdp+remap_schema导入后登录报ORA-01017: Invalid Username/password

环境说明&#xff1a;有个11.2.0.4的rac数据库&#xff0c;现需要把USR_OA克隆一份出来做测试&#xff0c;新用户名是TEST_OA&#xff0c;直接是expdp导出用户&#xff0c;再用impdpremap_schema生成TEST_OA&#xff0c; 业务人员使用PLSQL(版本12.0.1.1814) 登录TEST_OA时总…

Python程序设计 内置函数 日志模块

logging(日志) 日志记录是程序员工具箱中非常有用的工具。它可以帮助您更好地理解程序的流程&#xff0c;并发现您在开发过程中可能没有想到的场景。 日志为开发人员提供了额外的一组眼睛&#xff0c;这些眼睛不断关注应用程序正在经历的流程。它们可以存储信息&#xff0c;例…

ShardingProxy服务端分库分表

目录 一、为什么要有服务端分库分表&#xff1f; 二、ShardingProxy基础使用 1、部署ShardingProxy 2、配置常用分库分表策略 三、ShardingSphere中的分布式事务机制 1、什么是XA事务&#xff1f; 2、实战理解XA事务 3、如何在ShardingProxy中使用另外两种事务管理器&a…

【不要离开你的舒适圈】:猛兽才希望你落单,亲人总让你回家,4个维度全面构建舒适圈矩阵

单打独斗的英雄时代已经落幕 抱团取暖才是社会寒冬的良策 自然界中&#xff0c;每个物种都占据着自己的领地和生存空间。 生态位的差异决定了它们的生存方式&#xff0c;一旦离开领地&#xff0c;失去群体的庇护&#xff0c;就会沦为野兽的美餐。 人类社会同样存在隐形圈层…

数仓建模:金字塔原理在数仓建模分析中的应用

目录 1 金字塔原理 1.1 金子塔原理基本原理 1.2 金字塔内部结构 2 如何构建金字塔 2.1 金字塔塔尖构建 2.2 金字塔纵向层次构建 2.3 金字塔横向关系构建 2.3.1 归类分组 2.3.2 逻辑递进 2.4 小结 3 金字塔原理应用 3.1 数仓建模 3.1.1 数仓建模分析方法-自上而下…

OBOO鸥柏:液晶拼接大屏搭载节点盒分布式集中管控控制系统新技术

近年来&#xff0c;随着视频监控、会议系统及展示需求的快速增长&#xff0c;KVM分布式输入输出节点控制系统在各大行业中逐渐成为核心技术。OBOO鸥柏的液晶拼接大屏分布式输入输出节点控制系统&#xff08;WControl&#xff09;&#xff0c;以其创新的技术和卓越的用户体验&am…

在线刷题系统测试报告

一、项目背景 1. 本项目是一个在线刷题系统&#xff0c;灵感来源于力扣和牛客等刷题平台&#xff0c;旨在锻炼自己的代码能力和剖析系统整体结构与各模块之间关系的能力。系统支持用户注册与登录&#xff0c;查看题目列表与题目详情&#xff0c;在线提交代码并提供反馈。 2. 该…