我的编程空间,编程开发者的网络收藏夹
学习永远不晚

java重试机制实现方案

短信预约 -IT技能 免费直播动态提醒
省份

北京

  • 北京
  • 上海
  • 天津
  • 重庆
  • 河北
  • 山东
  • 辽宁
  • 黑龙江
  • 吉林
  • 甘肃
  • 青海
  • 河南
  • 江苏
  • 湖北
  • 湖南
  • 江西
  • 浙江
  • 广东
  • 云南
  • 福建
  • 海南
  • 山西
  • 四川
  • 陕西
  • 贵州
  • 安徽
  • 广西
  • 内蒙
  • 西藏
  • 新疆
  • 宁夏
  • 兵团
手机号立即预约

请填写图片验证码后获取短信验证码

看不清楚,换张图片

免费获取短信验证码

java重试机制实现方案

本文内容是目前团队内小磊同学对重试机制实现方案的梳理总结。

从为什么需要重试的背景开始,到重试的场景,大致的一些设计思路,最后通过两个成熟的retry组件进行案例讲解,理论+实战。

55ecfa354261c87ab7c220b308e3b955.png

背景

重试是系统提高容错能力的一种手段。在一次请求中,往往需要经过多个服务之间的调用,由于网络波动或者其他原因,请求可能无法正常到达服务端或者服务端的请求无法正常的返回,从而导致请求失败,这种失败往往可以通过重试的方式来解决。因此服务之间的重试机制提高了整个系统的故障恢复能力。

重试场景

典型的重试场景如下:

  1. 网络抖动问题造成的请求失败,通过重试提高成功率。

  2. 由于系统负载高原因导致请求变慢,导致请求超时,通过重试提高成功率。

  3. 由于系统故障或服务不可用导致请求没能成功,通过重试保证数据落地。

那么是不是所有请求都可以重试?
显然不是,重试依赖于接口的幂等性,假设一个接口多次使用相同参数调用会导致其违反数据约束,那么得到的结果可能会在我们预期之外。对于幂等不了解的伙伴,可以参看:高并发下接口幂等性解决方案

设计思路

如何设计一个优雅的重试机制?优雅的重试机制应该具备如下几点特点:

  • 无侵入:或者侵入低,这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现

  • 可配置:包括重试次数,重试的间隔时间,是否使用异步方式等

  • 通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用

模板方式

将重试机制的实现抽取成一个模板,预留接口。示例如下:

public abstract class MyRetryTemplate<T> {    //重试次数    private int retryTime;    //重试时间    private int sleepTime;    //重试时间是否倍数增长    private  boolean multiple = false;         public abstract T doBiz() throws Exception;     public T execute() throws InterruptedException {        for (int i = 1; i < retryTime + 1; i++) {            try {                return doBiz();            } catch (Exception e) {                System.out.println(e.getMessage());                if (multiple){                    Thread.sleep(sleepTime);                }                else{                    Thread.sleep(sleepTime * (i));                }            }        }        return null;    }     public T submit(ExecutorService executorService) {        Future submit = executorService.submit((Callable) () -> execute());        try {            return (T) submit.get();        } catch (InterruptedException | ExecutionException e) {            e.printStackTrace();        }         return null;    }     public MyRetryTemplate setRetryTime(int retryTime) {        this.retryTime = retryTime;        return this;    }     public MyRetryTemplate setSleepTime(int sleepTime) {        this.sleepTime = sleepTime;        return this;    }     public MyRetryTemplate setMultiple(boolean multiple) {        this.multiple = multiple;        return this;    }}

业务代码中的使用demo:

public void retryDemo() throws InterruptedException {    Object ans = new MyRetryTemplate() {        @Override        protected Object doBiz() throws Exception {            int n = (int) (Math.random() * 10);            System.out.println(n);              if (n > 3) {                throw new Exception("generate value bigger then 3! need retry");            }              return n;        }    }.setRetryTime(10).setSleepTime(10).execute();    System.out.println(ans);}
  • 优点

    • 实现简单

    • 使用灵活

  • 缺点

    • 代码侵入性高

    • 代码臃肿

切面方式

自定义注解,在需要重试的方法上标注。然后在切面中实现重试的逻辑,重试配置参数可以写在注解参数中。在模板方式的基础上,通过spring的aop实现该方式如下:

自定义注解:

@Documented@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Retry {        int retryTime() default 0;          int sleepTime() default 0;          boolean asyn() default false;            boolean multiple() default false;}

切面逻辑:

@Aspect@Componentpublic class RetryAspect {     ExecutorService executorService = new ThreadPoolExecutor(3, 5,            1, TimeUnit.MINUTES,            new LinkedBlockingQueue<Runnable>());      @Around(value = "@annotation(retry)")    public Object execute(ProceedingJoinPoint joinPoint, Retry retry) throws Exception {        RetryTemplate retryTemplate = new RetryTemplate() {            @Override            protected Object doBiz() throws Throwable {                return joinPoint.proceed();            }        }.setRetryCount(retry.count()).setSleepTime(retry.sleepTime());                if (retry.asyn()) {            return retryTemplate.submit(executorService);        } else {            return retryTemplate.execute();        }    }}

注解使用示例:

@Retry(retryTime=4, sleepTime=10000, multiple=true)public void retryDemo() throws InterruptedException {   int n = (int) (Math.random() * 10);    System.out.println(n);     if (n > 3) {        throw new Exception("generate value bigger then 3! need retry");    }}
  • 优点

    • 代码侵入性低

  • 缺点

    • 某些方法无法被切面拦截的场景无法覆盖(如spring-aop无法切私有方法,final方法)

    • 直接使用aspecj有些复杂;如果用spring-aop,则只能切被spring容器管理的bean

定时任务

通过定时任务定时执行某种需要重试的业务。以订单支付为例,定时扫描数据库中一定时间范围内的所有锁定状态订单,然后查询支付中心这笔订单的状态,此例中重试间隔时间就是定时任务的间隔时间,重试停止条件就是查不到对应的订单。

伪代码:

@Xxljob(cron = "50 */3  * * * ?")public void getOrderStatus(){    // 计算订单查询开始时间    String startTime = DateUtils.addMinut(new Date, -30, "yyyy-MM-dd hh:mm:ss");    // 查询startTime之后的锁定状态订单    List<Order> orderList = orderMapper.getOrderList(startTime, "L");    for (Order order : orderList) {        // 调用支付中心接口查询订单支付状态        Order order = orderCenter.getOrderStatus(order);        if ("S".equals(order.getStatus())) {            // 修改支付状态            orderMapper.update();        }    }}
  • 优点

    • 使用简单

    • 解耦

    • 侵入性低

  • 缺点

    • 重试时间间隔固定

消息队列重试

参考:https://blog.csdn.net/qq_37513473/article/details/102591717

可以通过消息队列中的延时队列和死信交换机实现重试队列,这里通过rabbitmq实现。在介绍重试队列实现之前,先了解一下延迟队列与死信队列

死信交换机

队列中的消息可能会变成死信消息(dead-lettered),进而当以下几个事件任意一个发生时,消息将会被重新发送到一个交换机:

  • 消息被消费者使用basic.reject或basic.nack方法并且requeue参数值设置为false的方式进行消息确认(negatively acknowledged)

  • 消息由于消息有效期(per-message TTL)过期

  • 消息由于队列超过其长度限制而被丢弃

死信消息将被队列的死信交换机路由到其他队列

延迟队列

延迟队列可以解决很多特定场景下,带时间属性的任务需求。延迟队列一般应用于需要延迟工作的场景,比如:

  1. 订单下单后未支付需要延迟一定时间释放。

  2. 预定会议后,需要在预定的时间点前十分钟通知各个与会人员参加会议

  3. 账单在一周内未支付,则自动结算

  4. 用户注册成功后,如果三天内没有登陆则进行短信提醒

这些场景都有一个特点,就是需要在某个事件发生之后或者之前的指定时间点完成某一项任务。队列内的消息有序,且会在指定的时间后被取出消费。

在rabbitmq中,延迟队列的实现依赖它的特性——TTL(time to live),TTL是RabbitMQ中一个消息或者队列的属性,表明一条消息或者该队列中的所有消息的最大存活时间,单位是毫秒。换句话说,如果一条消息设置了TTL属性或者进入了设置TTL属性的队列,那么这条消息如果在TTL设置的时间内没有被消费,则会成为“死信”。如果同时配置了队列的TTL和消息的TTL,那么较小的那个值将会被使用。

延迟队列的实现

rabbitmq本身没有延迟队列,我们可以通过设置队列和消息的TTL属性与死信交换机实现一个延迟队列。将消息设置TTL后,不让消费者进行消费,消息过期后会自动通过死信交换机路由到其他队列,让消费者消费死信交换机路由后的队列中的消息,这样就实现了一个延迟队列。16b9ba415b4f1375c6f9befff20c7f06.png

重试实现

在了解了延迟队列的实现方式后,重试实现起来也相差不大。将需要重试的业务发送一条消息到队列A中,消费者进行消费,如果处理失败,则再发送一条消息到一个延迟队列B,队列B中的消息过期后通过交换机路由重新回到队列A中,这就实现了重试。这种方法中,重试方法的等待时间就是消息的过期时间,而重试最大等待时间为延迟队列的TTL,借助redis等类似的组件缓存重试次数,达到限制方法重试次数的方法。c01ef8f793c650cdb3d7b9f9e2844857.png

成熟的重试组件

spring-retry

该项目为Spring应用程序提供声明式重试支持。它用于Spring批处理、Spring集成等。命令式重试也支持显式使用。它主要是针对可能抛出异常的一些调用操作,进行有策略的重试

依赖引入

<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>1.2.5-RELEASE</version></dependency>

重试示例

启动类上开启重试

@SpringBootApplication@EnableAspectJAutoProxy@EnableRetrypublic class DemoApplication {    public static void main(String[] args) {        SpringApplication.run(DemoApplication.class, args);    }}

在需要重试的方法上标注@Retryable注解,注解中参数含义:

  • value:当方法抛出此类异常时进行重试。示例中配置的是RuntimeException,也就是在发生运行时异常时进行重试

  • maxAttempts:最大执行次数。示例中配置的是3,当方法发生RuntimeException异常后最多重试2次

  • backoff:重试等待策略。本示例配置含义为重试等待时间为5s,每次等待时间翻两倍

@Servicepublic class RemoteService {        @Retryable(value = RuntimeException.class,               maxAttempts = 3,               backoff = @Backoff(delay = 5000L, multiplier = 2))    public void call() {        System.out.println("Call something...");        throw new RuntimeException("RPC调用异常");    }         @Recover    public void recover(RuntimeException e) {        System.out.println("Call recover...");    }}

如果达到最大重试次数还没请求成功,这种情况可使用@Recover进行熔断补偿,该方法会在全部重试失败调用

缺陷

spring-retry 存在两个不友好设计:

  1. 重试实体限定为 Throwable 子类,说明重试针对的是可捕捉的功能异常为设计前提的,但是我们希望依赖某个数据对象实体作为重试实体,但 sping-retry框架必须强制转换为Throwable子类。

  2. 如果你要以返回值的某个状态来判定是否需要重试,可能只能通过自己判断返回值然后显式抛出异常了

guava-retrying

guava retrying模块提供了一种通用方法,用于重试具有特定停止、重试和异常处理功能的任意Java代码,这些功能通过guava的谓词匹配得到了增强。

依赖引入

<!-- https://mvnrepository.com/artifact/com.github.rholder/guava-retrying --><dependency>    <groupId>com.github.rholder</groupId>    <artifactId>guava-retrying</artifactId>    <version>2.0.0</version></dependency>

入门案例

public class RetryDemoTask {    public static boolean retryTask(String param)  {    int i = RandomUtils.nextInt(0,11);    if (i < 2) {      throw new IllegalArgumentException("参数异常");    }else if (i  < 5){      return true;    }else if (i < 7){      return false;    }else{        throw new RemoteAccessException("大于2,抛出自定义异常");    }  }}
public void fun01(){    // RetryerBuilder 构建重试实例 retryer,可以设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔    Retryer<Boolean> retryer = RetryerBuilder.<Boolean> newBuilder()            .retryIfExceptionOfType(RemoteAccessException.class)//设置异常重试源            .retryIfResult(res-> res==false)  //设置根据结果重试            .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) //设置等待间隔时间            .withStopStrategy(StopStrategies.stopAfterAttempt(3)) //设置最大重试次数            .build();    try {      retryer.call(() -> RetryDemoTask.retryTask("abc"));    } catch (Exception e) {      e.printStackTrace();    }}

关键设置说明:retryIfExceptionOfType:发生指定异常时重试 retryIfResult:设置一个断言表达式,例子中结果为false重试 withWaitStrategy:重试等待策略,可设置的值如下:

  • WaitStrategy.fixedWait():每次等待间隔固定

  • WaitStrategy.randomWait():每次等待间隔时间为设置的最小时间~最大时间之间的随机时间

  • WaitStrategy.incrementingWait():每次等待时间递增

  • WaitStrategy.exponentialWait():每次等待时间倍数增长,不超过设置的最大时间

  • WaitStrategy.fibonacciWait():每次等待时间呈菲波那契数列增长

  • WaitStrategy.exceptionWait():发生指定异常时等待自定义时间

withStopStrategy:重试停止策略,可设置的值如下:

  • StopStrategies.stopAfterAttempt():达到重试次数上线后停止重试

  • StopStrategies.stopAfterDelay():超过最大重试时间后停止重试

withAttemptTimeLimiter:重试限制器,默认两种提供两种实现:

  • NoAttemptTimeLimit:无限制,直接调用回调方法

  • FixedAttemptTimeLimit:固定时间限制处理器,超时取消

withBlockStrategy:阻塞策略。阻塞策略配置每次重试之前如何阻塞流程,默认是线程休眠,guava-retrying只提供了一种阻塞策略:

  • ThreadSleepStrategy:线程休眠(默认)

缺陷

github中该项目已经很久没更新维护了,虽然很久没维护,但不影响使用。

总结

上面介绍了几种常见重试机制的实现方法以及两种成熟的重试组件,除了定时任务重试,其余的方案他们都存在一个共同的问题,就是当服务器宕机或者其他情况导致方法没有进行重试。

在一些一定要进行重试补偿的场景中,上述方案无法保证方法一定进行重试,可以将重试数据持久化,通过定时任务+重试组合解决这个问题。

以上就是本次文章的全部内容,感谢你阅读,如果文章对你有所帮助,点赞支持一下,感谢你的慷慨~

来源地址:https://blog.csdn.net/t194978/article/details/132001159

免责声明:

① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

java重试机制实现方案

下载Word文档到电脑,方便收藏和打印~

下载Word文档

猜你喜欢

【重试】Java 中的 7 种重试机制

随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为
2023-08-21

Java中重试机制的方式有哪些

今天小编给大家分享一下Java中重试机制的方式有哪些的相关知识点,内容详细,逻辑清晰,相信大部分人都还太了解这方面的知识,所以分享这篇文章给大家参考一下,希望大家阅读完这篇文章后有所收获,下面我们一起来了解一下吧。重试机制在分布式系统中,或
2023-07-05

Golang函数重试机制实现代码

Go中函数重试机制的实现在Go中,可通过错误处理和延迟机制实现函数重试。该机制包含自定义错误类型、函数内错误处理和延迟设置。代码示例展示了处理重试错误并设置延迟时间的实现。其他考虑因素包括重试次数限制、重试规则、错误记录和并发控制。该机制可根据需求定制,以满足不同场景。
Golang函数重试机制实现代码
2024-04-23

深入浅出Java中重试机制的多种方式

重试机制在分布式系统中,或者调用外部接口中,都是十分重要的。重试机制可以保护系统减少因网络波动、依赖服务短暂性不可用带来的影响,让系统能更稳定的运行的一种保护机制。本文就来和大家聊聊Java中重试机制的多种方式
2023-03-14

java重试机制使用RPC要考虑什么

这篇文章主要介绍“java重试机制使用RPC要考虑什么”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“java重试机制使用RPC要考虑什么”文章能帮助大家解决问题。1 为什么重试如果简单对一个RPC交
2023-07-05

如何使用 Golang 实现 HTTP 文件上传的重试机制?

使用 go 实现 http 文件上传重试机制:使用 client.do() 方法发送请求。在发生错误时,等待指定的秒数(retrywaitseconds)。最多重试 maxretries 次。如果重试次数达到上限,则返回错误 "maximu
如何使用 Golang 实现 HTTP 文件上传的重试机制?
2024-05-14

Flutter:WebSocket封装-实现心跳、重连机制

前言Permalink Flutter简介 Flutter 是 Google推出并开源的移动应用开发框架,主打跨平台、高保真、高性能。开发者可以通过 Dart语言开发 App,一套代码同时运行在 iOS 和 Android平台。 Flutt
2023-08-30

java超时机制怎么实现

在Java中,可以使用java.util.Timer和java.util.concurrent.Executors等类来实现超时机制。java.util.Timer:创建一个定时器,使用schedule方法来安排超时操作。可以使用Timer
java超时机制怎么实现
2024-02-29

Java中怎么实现SPI机制

Java中怎么实现SPI机制,很多新手对此不是很清楚,为了帮助大家解决这个难题,下面小编将为大家详细讲解,有这方面需求的人可以来学习下,希望你能有所收获。2 什么是SPI机制SPI是Service Provider Interface 的简
2023-06-16

java方法重载怎么实现

这篇“java方法重载怎么实现”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“java方法重载怎么实现”文章吧。1、概念让类以
2023-06-30

怎么用spring retry方法调用失败重试机制

这篇文章主要介绍“怎么用spring retry方法调用失败重试机制”的相关知识,小编通过实际案例向大家展示操作过程,操作方法简单快捷,实用性强,希望这篇“怎么用spring retry方法调用失败重试机制”文章能帮助大家解决问题。前言很多
2023-06-29

编程热搜

  • Python 学习之路 - Python
    一、安装Python34Windows在Python官网(https://www.python.org/downloads/)下载安装包并安装。Python的默认安装路径是:C:\Python34配置环境变量:【右键计算机】--》【属性】-
    Python 学习之路 - Python
  • chatgpt的中文全称是什么
    chatgpt的中文全称是生成型预训练变换模型。ChatGPT是什么ChatGPT是美国人工智能研究实验室OpenAI开发的一种全新聊天机器人模型,它能够通过学习和理解人类的语言来进行对话,还能根据聊天的上下文进行互动,并协助人类完成一系列
    chatgpt的中文全称是什么
  • C/C++中extern函数使用详解
  • C/C++可变参数的使用
    可变参数的使用方法远远不止以下几种,不过在C,C++中使用可变参数时要小心,在使用printf()等函数时传入的参数个数一定不能比前面的格式化字符串中的’%’符号个数少,否则会产生访问越界,运气不好的话还会导致程序崩溃
    C/C++可变参数的使用
  • css样式文件该放在哪里
  • php中数组下标必须是连续的吗
  • Python 3 教程
    Python 3 教程 Python 的 3.0 版本,常被称为 Python 3000,或简称 Py3k。相对于 Python 的早期版本,这是一个较大的升级。为了不带入过多的累赘,Python 3.0 在设计的时候没有考虑向下兼容。 Python
    Python 3 教程
  • Python pip包管理
    一、前言    在Python中, 安装第三方模块是通过 setuptools 这个工具完成的。 Python有两个封装了 setuptools的包管理工具: easy_install  和  pip , 目前官方推荐使用 pip。    
    Python pip包管理
  • ubuntu如何重新编译内核
  • 改善Java代码之慎用java动态编译

目录