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

在我们的项目中,为了提高程序的健壮性,很多时候都需要有重试机制进行兜底,最多就场景就比如调用远程的服务,调用中间件服务等,因为网络是不稳定的,所以在进行远程调用的时候偶尔会产生超时的异常,所以一般来说我们都会通过手动去写一些重试的代码去进行兜底,而这些重试的代码其实都是模板化的,因此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);

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python之socket网络编程

华子目录 引言什么是socketsocket套接字类型TCP和UDP socket服务端核心组件1.创建socket对象2.绑定地址和端口3.监听连接4.接受连接5.接受client端消息client_sock.revc(1024)6.发送响应给client端6.1client_sock.send()6.2client_sock.sendall() 7.关闭client端连接8.关闭serv…

flutter 使用三方/自家字体

将字体放入assets/fonts下 在pubspec.yaml文件中flutter下添加如下代码&#xff1a; flutter:fonts:- family: MyCustomFontfonts:- asset: assets/fonts/MyCustomFont.ttf 在flutter Text widget中使用字体 import package:flutter/material.dart;void main() > runApp(…

PyQt 入门教程(3)基础知识 | 3.2、加载资源文件

文章目录 一、加载资源文件1、PyQt5加载资源文件2、PyQt6加载资源文件 一、加载资源文件 常见的资源文件有图像与图标&#xff0c;下面分别介绍下加载资源文件的常用方法 1、PyQt5加载资源文件 2、PyQt6加载资源文件 PyQt6版本暂时没有提供pyrcc工具&#xff0c;下面介绍下在不…

js中map,filter,find,foreach的用法介绍

js中map&#xff0c;filter&#xff0c;find&#xff0c;foreach的用法介绍 在 JavaScript 中&#xff0c;数组提供了一些常用的迭代方法&#xff0c;如 map、filter、find 和 forEach&#xff0c;这些方法允许你对数组中的每个元素进行操作&#xff0c;下面是它们的用法和区别…

抖音解压视频素材宝库

在快节奏的生活中&#xff0c;解压视频成为抖音上的热门内容类型&#xff0c;想要制作出精彩的解压视频&#xff0c;优质素材必不可少。今天&#xff0c;为大家推荐几个超棒的抖音解压视频素材网站&#xff0c;让你的创作之路轻松无忧&#xff01; 蛙学网 作为国内知名的短视频…

【Canvas与化学】铁元素图标

【成图】 【代码】 <!DOCTYPE html> <html lang"utf-8"> <meta http-equiv"Content-Type" content"text/html; charsetutf-8"/> <head><title>铁元素图标Draft1</title><style type"text/css"…

域渗透AD 示例场景漏洞 Kerberos Bronze Bit 【CVE-2020-17049】漏洞

背景 漏洞原理 漏洞复现 约束性委派攻击绕过 基于资源的约束性委派攻击绕过 漏洞预防和修复 背景 Kerberos Bronze Bit (CVE-2020-17049) 漏洞是国外安全公司 Netspi 安全研究员Jake Karnes 发现的一个Kerberos安全功能绕过漏洞。该漏洞存在的原因在于KDC在确定Kerberos服…

YoloV10改进:Block改进|使用ContextAggregation模块改善C2f模块|即插即用

摘要 在计算机视觉领域&#xff0c;目标检测与实例分割任务一直是研究的热点。YoloV10作为目标检测领域的佼佼者&#xff0c;凭借其出色的性能和效率赢得了广泛的认可。然而&#xff0c;随着技术的不断进步&#xff0c;如何进一步提升YoloV10的性能成为了我们追求的目标。近期…

Java爬虫之使用Selenium WebDriver 爬取数据

这里写自定义目录标题 Selenium WebDriver简介一、安装部署二、Java项目中使用1.引入依赖2.示例代码 三、WebDriver使用说明1.WebDriver定位器2.常用操作3.使用 cookie4.键盘与鼠标操作 Selenium WebDriver简介 Selenium WebDriver 是一种用于自动化测试 Web 应用程序的工具。…

【实战指南】Vue.js 介绍组件数据绑定路由构建高效前端应用

学习总结 1、掌握 JAVA入门到进阶知识(持续写作中……&#xff09; 2、学会Oracle数据库入门到入土用法(创作中……&#xff09; 3、手把手教你开发炫酷的vbs脚本制作(完善中……&#xff09; 4、牛逼哄哄的 IDEA编程利器技巧(编写中……&#xff09; 5、面经吐血整理的 面试技…

ollama + fastgpt+m3e本地部署

ollama fastgptm3e本地部署 开启WSL更新wsl安装ubuntu docker下载修改docker镜像源开启WSL integration 安装fastgpt先创建一个文件夹来放置一些配置文件用命令下载fastgpt配置文件用命令下载docker的部署文件 启动容器M3E下载ollama下载oneapi配置登录oneapi配置ollama渠道配…

聊聊零基础如何开始学习鸿蒙开发技术

鸿蒙系统是一款分布式操作系统&#xff0c;其适用范围非常广泛&#xff0c;从智能手机到家用电器&#xff0c;再到工业设备&#xff0c;都能找到应用场景。特别是在智能家居领域&#xff0c;鸿蒙系统可以实现不同设备之间的无缝连接和协同工作&#xff0c;提供更加智能和便利的…

Flink On kubernetes

Apache Flink 是一个分布式流处理引擎&#xff0c;它提供了丰富且易用的API来处理有状态的流处理应用&#xff0c;并且在支持容错的前提下&#xff0c;高效、大规模的运行此类应用。通过支持事件时间&#xff08;event-time&#xff09;、计算状态&#xff08;state&#xff09…

数据治理为何如此简单?

欢迎来文末免费获取数据治理相关PPT和文档 引言 随着大数据技术的迅速发展&#xff0c;企业积累的数据量呈现爆炸式增长。有效的数据管理已经成为企业提高决策效率、增强竞争优势的重要手段。在这样的背景下&#xff0c;数据治理逐渐成为企业数据管理中不可或缺的一环。它不仅…

Vivado - Aurora 8B/10B IP

目录 1. 简介 2. 设计调试 2.1 Physical Layer 2.2 Link Layer 2.3 Receiver 2.4 IP 接口 2.5 调试过程 2.5.1 Block Design 2.5.2 释放 gt_reset 2.5.3 观察数据 3. 实用技巧 3.1 GT 坐标与布局 3.1.1 选择器件并进行RTL分析 3.1.2 进入平面设计 3.1.3 收发器布…

【二刷hot-100】day1

目录 1.两数之和 2.字母异位词分组 3.字母异位词分组 4.最长连续序列 5.移动零 6.盛最多水的容器 7.三数之和 8.接雨水 1.两数之和 class Solution {public int[] twoSum(int[] nums, int target) {Map<Integer,Integer> mapnew HashMap<>();for (int i0;i<…

LeakCanary

LeakCanary 文章目录 LeakCanary一、内容1. 使用方法2. 工作原理3.工作流程 参考资料 一、内容 LeakCanary 是在 Android 项目中&#xff0c;用于检测内存泄露&#xff0c;优化性能的工具。 1. 使用方法 本文使用版本为 2.5 版本&#xff0c;相比于 2.0 之前的版本&#xff…

嵌入式:Keil的Code、RW、RO、ZI段的解析

相关阅读 嵌入式https://blog.csdn.net/weixin_45791458/category_12768532.html // 例1 int main(void) {HAL_Init(); /* 初始化HAL库 */sys_stm32_clock_init(RCC_PLL_MUL9); /* 设置时钟,72M */delay_init(72); …

[PHP]重复的Notice错误信息

<?php $a []; var_dump($a[name]);执行结果&#xff1a; 原因&#xff1a; display_errors和error_reporting都打开了Notice错误信息

前缀和一>寻找数组的中心下标

1.题目&#xff1a; 2.解析&#xff1a; 如果暴力解法时间复杂度是O(N^2)&#xff0c;定个&#xff0c;i&#xff0c;遍历左边右边&#xff1b; 这里可以优化为前缀和的做法&#xff0c;其实就是个动态规划。 代码&#xff1a; public int pivotIndex(int[] nums) {int n n…