编程技术是改变世界的力量。
本站
当前位置:网站首页 > 后端语言 > 正文

Java教程:重试实现高可用一览 java高可用设计方案

gowuye 2024-04-03 16:15 12 浏览 0 评论

1、背景介绍

随着互联网的发展项目中的业务功能越来越复杂,有一些基础服务我们不可避免的会去调用一些第三方的接口或者公司内其他项目中提供的服务,但是远程服务的健壮性和网络稳定性都是不可控因素。在测试阶段可能没有什么异常情况,但上线后可能会出现调用的接口因为内部错误或者网络波动而出错或返回系统异常,因此我们必须考虑加上重试机制。

重试机制可以提高系统的健壮性,并且减少因网络波动依赖服务临时不可用带来的影响,让系统能更稳定的运行。

2、测试环境

2.1 模拟远程调用

本文会用如下方法来模拟远程调用的服务,其中每调用3次才会成功一次:

@Slf4j
@Service
public class RemoteService {
    /**
     * 记录调用次数
     */
    private final static AtomicLong count = new AtomicLong(0);

    /**
     * 每调用3次会成功一次
     */
    public String hello() {
        long current = count.incrementAndGet();
        System.out.println("第" + current +"次被调用");
        if (current % 3 != 0) {
            log.warn("调用失败");
            return "error";
        }
        return "success";
    }
}

2.2 单元测试

编写单元测试:

@SpringBootTest
public class RemoteServiceTest {

    @Autowired
    private RemoteService remoteService;

    @Test
    public void hello() {
        for (int i = 1; i < 9; i++) {
            System.out.println("远程调用:" + remoteService.hello());
        }
    }
}

执行后查看结果:验证是否调用3次才成功一次

同时在上边的单元测试中用for循环进行失败重试:在调用的时候如果失败则会进行了重复调用,直到成功

@Test
public void testRetry() {
for (int i = 1; i < 9; i++) {
String result = remoteService.hello();
if (!result.equals("success")) {
System.out.println("调用失败");
continue;
}
System.out.println("远程调用成功");
break;
}
}

上述代码看上去可以解决问题,但实际上存在一些弊端:

  • 由于没有重试间隔,很可能远程调用的服务还没有从网络异常中恢复,所以有可能接下来的几次调用都会失败
  • 代码侵入式太高,调用方代码不够优雅
  • 项目中远程调用的服务可能有很多,每个都去添加重试会出现大量的重复代码

3、自己动手使用AOP实现重试

考虑到以后可能会有很多的方法也需要重试功能,咱们可以将重试这个共性功能通过AOP来实现:

使用AOP来为目标调用设置切面,即可在目标方法调用前后添加一些重试的逻辑。

1)创建一个注解:用来标识需要重试的方法

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Retry {
    /**
     * 最多重试次数
     */
    int attempts() default 3;

    /**
     * 重试间隔
     */
    int interval() default 1;
}

2)在需要重试的方法上加上注解:

//指定重试次数和间隔
@Retry(attempts = 4, interval = 5)
public String hello() {
    long current = count.incrementAndGet();
    System.out.println("第" + current +"次被调用");
    if (current % 3 != 0) {
        log.warn("调用失败");
        return "error";
    }
    return "success";
}

3)编写AOP切面类,引入依赖:

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
</dependency>
/**
 * 重试切面类
 */
@Aspect
@Component
@Slf4j
public class RetryAspect {

    /**
     * 定义切入点
     */
    @Pointcut("@annotation(cn.itcast.annotation.Retry)")
    private void pt() {}

    /**
     * 定义重试的共性功能
     */
    @Around("pt()")
    public Object retry(ProceedingJoinPoint joinPoint) throws InterruptedException {
        //获取@Retry注解上指定的重试次数和重试间隔
        MethodSignature sign = (MethodSignature) joinPoint.getSignature();
        Retry retry = sign.getMethod().getAnnotation(Retry.class);
        int maxRetry = retry.attempts(); //最多重试次数
        int interval = retry.interval(); //重试间隔

        Throwable ex = new RuntimeException();//记录重试失败异常
        for (int i = 1; i <= maxRetry; i++) {
            try {
                Object result = joinPoint.proceed();
                //第一种失败情况:远程调用成功返回,但结果是失败了
                if (result.equals("error")) {
                    throw new RuntimeException("远程调用返回失败");
                }
                return result;
            } catch (Throwable throwable) {
                //第二种失败情况,远程调用直接出现异常
                ex = throwable;
            }
            //按照注解上指定的重试间隔执行下一次循环
            Thread.sleep(interval * 1000);
            log.warn("调用失败,开始第{}次重试", i);
        }
        throw new RuntimeException("重试次数耗尽", ex);
    }
}

4)编写单元测试

@Test
public void testAOP() {
    System.out.println(remoteService.hello());
}

调用失败后:等待5毫秒后会进行重试,直到重试到达指定的上限或者调用成功

这样即不用编写重复代码,实现上也比较优雅了:一个注解就实现重试。

4、站在巨人肩上:Spring Retry

目前在Java开发领域,Spring框架基本已经是企业开发的事实标准。如果项目中已经引入了Spring,那咱们就可以直接使用Spring Retry,可以比较方便快速的实现重试功能,还不需要自己动手重新造轮子。

4.1 简单使用

下面咱们来一块来看看这个轮子究竟好不好使吧。

1)先引入重试所需的jar包

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

2)开启重试功能:在启动类或者配置类上添加@EnableRetry注解:

@SpringBootApplication
@EnableRetry
public class RemoteApplication {

    public static void main(String[] args) {
        SpringApplication.run(RemoteApplication.class);
    }
}

3)在需要重试的方法上添加@Retryable注解

/**
 * 每调用3次会成功一次
 */
@Retryable //默认重试三次,重试间隔为1秒
public String hello() {
    long current = count.incrementAndGet();
    System.out.println("第" + current + "次被调用");
    if (current % 3 != 0) {
        log.warn("调用失败");
        throw new RuntimeException("发生未知异常");
    }
    return "success";
}

4)编写单元测试,验证效果

@Test
public void testSpringRetry() {
    System.out.println(remoteService.hello());
}

通过日志可以看到:第一次调用失败后,经过两次重试,重试间隔为1s,最终调用成功

4.2 更灵活的重试设置

4.2.1 指定异常重试和次数

Spring的重试机制还支持很多很有用的特性:

  • 可以指定只对特定类型的异常进行重试,这样如果抛出的是其它类型的异常则不会进行重试,就可以对重试进行更细粒度的控制。
  • //@Retryable //默认为空,会对所有异常都重试 @Retryable(value = {MyRetryException.class}) //只有出现MyRetryException才重试 public String hello(){ //... }
  • 也可以使用include和exclude来指定包含或者排除哪些异常进行重试。
  • @Retryable(exclude = {NoRetryException.class}) //出现NoRetryException异常不重试
  • 可以用maxAttemps指定最大重试次数,默认为3次。
  • @Retryable(maxAttempts = 5)

4.2.2 指定重试回退策略

如果因为网络波动导致调用失败,立即重试可能还是会失败,最优选择是等待一小会儿再重试。决定等待多久之后再重试的方法叫做重试回退策略。通俗的说,就是每次重试是立即重试还是等待一段时间后重试。

默认情况下是立即重试,如果要指定策略则可以通过注解中backoff属性来快速实现:

  • 添加第二个重试方法,改为调用4次才成功一次。
  • 指定重试回退策略为:延迟5秒后进行第一次重试,后面重试间隔依次变为原来的2倍(10s, 15s)
  • 这种策略一般称为指数回退,Spring中也提供很多其他方式的策略(实现BackOffPolicy接口的都是)
/**
 * 每调用4次会成功一次
 */
@Retryable(
        maxAttempts = 3, //指定重试次数
        //调用失败后,等待5s重试,后面重试间隔依次变为原来的2倍
        backoff = @Backoff(delay = 5000, multiplier = 2))
public String hello2() {
    long current = count.incrementAndGet();
    System.out.println("第" + current + "次被调用");
    if (current % 4 != 0) {
        log.warn("调用失败");
        throw new RuntimeException("发生未知异常");
    }
    return "success";
}

编写单元测试验证:

@Test
public void testSpringRetry2() {
    System.out.println(remoteService.hello2());
}

4.2.3 指定熔断机制

重试机制还支持使用@Recover 注解来进行善后工作:当重试达到指定次数之后,会调用指定的方法来进行日志记录等操作。

在重试方法的同一个类中编写熔断实现:

/**
 * 每调用4次会成功一次
 */
@Retryable(
        maxAttempts = 3, //指定重试次数
        //调用失败后,等待5s重试,后面重试间隔依次变为原来的2倍
        backoff = @Backoff(delay = 5000, multiplier = 2))
public String hello2() {
    long current = count.incrementAndGet();
    System.out.println("第" + current + "次被调用");
    if (current % 4 != 0) {
        log.warn("调用失败");
        throw new RuntimeException("发生未知异常");
    }
    return "success";
}

/**
 * 熔断机制:达到最多重试次数后,会调用 recover() 方法
 * @param ex 异常
 */
@Recover
public String recover(RuntimeException ex) {
    log.info("execute recover...");
    log.warn("重试到达上限", ex);
    return "final error";
}
注意:
1、@Recover注解标记的方法必须和被@Retryable标记的方法在同一个类中
2、重试方法抛出的异常类型需要与recover方法参数类型保持一致
3、recover方法返回值需要与重试方法返回值保证一致
4、recover方法中不能再抛出Exception,否则会报无法识别该异常的错误

总结

通过以上几个简单的配置,可以看到Spring Retry重试机制考虑的比较完善,比自己写AOP实现要强大很多。

4.3 弊端

Spring Retry虽然功能强大使用简单,但是也存在一些不足,Spring的重试机制只支持对异常进行捕获,而无法对返回值进行校验,具体看如下的方法:

1、方法执行失败,但没有抛出异常,只是在返回值中标识失败了(return error;)
/**
 * 每调用3次会成功一次
 */
@Retryable
public String hello3() {
    long current = count.incrementAndGet();
    System.out.println("第" + current +"次被调用");
    if (current % 3 != 0) {
        log.warn("调用失败");
        return "error";
    }
    return "success";
}
2、因此就算在方法上添加@Retryable,也无法实现失败重试

编写单元测试:

@Test
public void testSpringRetry3() {
    System.out.println(remoteService.hello3());
}

输出结果:只会调用一次,无论成功还是失败

5、另一个巨人谷歌 guava-retrying

5.1 Guava 介绍

Guava是一个基于Java的开源类库,其中包含谷歌在由他们很多项目使用的核心库。这个库目的是为了方便编码,并减少编码错误。这个库提供用于集合,缓存,并发性,常见注解,字符串处理,I/O和验证的实用方法。

源码地址:https://github.com/google/guava

优势:

  • 标准化 - Guava库是由谷歌托管。
  • 高效 - 可靠,快速和有效的扩展JAVA标准库
  • 优化 -Guava库经过高度的优化。

当然,此处咱们主要来看下 guava-retrying 功能。

5.2 使用guava-retrying

guava-retrying是Google Guava库的一个扩展包,可以对任意方法的调用创建可配置的重试。该扩展包比较简单,也已经好多年没有维护,但这完全不影响它的使用,因为功能已经足够完善。

源码地址:https://github.com/rholder/guava-retrying

和Spring Retry相比,Guava Retry具有更强的灵活性,并且能够根据返回值来判断是否需要重试。

1)添加依赖坐标

<!--guava retry是基于guava实现的,因此需要先添加guava坐标-->
<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <!--继承了SpringBoot后,父工程已经指定了版本-->
	<!--<version>29.0-jre</version>-->
</dependency>

<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>

2)编写远程调用方法,不指定任何Spring Retry中的注解

/**
 * 每调用3次会成功一次
 */
public String hello4() {
    long current = count.incrementAndGet();
    System.out.println("第" + current + "次被调用");
    if (current % 3 != 0) {
        log.warn("调用失败");
        //throw new RuntimeException("发生未知异常");
        return "error";
    }
    return "success";
}

3)编写单元测试:创建Retryer实例,指定如下几个配置

  • 出现什么类型异常后进行重试:retryIfException()
  • 返回值是什么时进行重试:retryIfResult()
  • 重试间隔:withWaitStrategy()
  • 停止重试策略:withStopStrategy()
@Test
public void testGuavaRetry() {
    Retryer<String> retryer = RetryerBuilder.<String>newBuilder()
            .retryIfException() //无论出现什么异常,都进行重试
            //返回结果为 error时,进行重试
            .retryIfResult(result -> Objects.equals(result, "error"))
            //重试等待策略:等待5s后再进行重试
            .withWaitStrategy(WaitStrategies.fixedWait(5, TimeUnit.SECONDS))
            //重试停止策略:重试达到5次
            .withStopStrategy(StopStrategies.stopAfterAttempt(5))
            .build();
}

4)调用方法,验证重试效果

try {
    retryer.call(() -> {
        String result = remoteService.hello4();
        System.out.println(result);
        return result;
    });
} catch (Exception e) {
    System.out.println("exception:" + e);
}
另外,也可以修改原始方法的失败返回实现:发现不管是抛出异常失败还是返回error失败,都能进行重试

另外,guava-retrying还有很多更灵活的配置和使用方式:

  1. 通过retryIfException 和 retryIfResult 来判断什么时候进行重试,同时支持多个且能兼容
  2. 设置重试监听器RetryListener,可以指定发生重试后,做一些日志记录或其他操作
  3. .withRetryListener(new RetryListener() { @Override public <V> void onRetry(Attempt<V> attempt) { System.out.println("RetryListener: 第" + attempt.getAttemptNumber() + "次调用"); } }) //也可以注册多个RetryListener,会按照注册顺序依次调用

5.3 弊端

虽然guava-retrying提供更灵活的使用,但是官方没有提供注解方式,频繁使用会有点麻烦。大家可以自己动手通过Spring AOP将实现封装为注解方式。

6、微服务架构中的重试(Feign+Ribbon)

在日常开发中,尤其是在微服务盛行的年代,我们在调用外部接口时,经常会因为第三方接口超时、限流等问题从而造成接口调用失败,那么此时我们通常会对接口进行重试,可以使用Spring Cloud中的Feign+Ribbon进行配置后快速的实现重试功能,经过简单配置即可:

spring:
  cloud:
   loadbalancer:
      retry:
        enabled: true #开启重试功能
ribbon:
  ConnectTimeout: 2000 #连接超时时间,ms
  ReadTimeout: 5000 #等待请求响应的超时时间,ms
  MaxAutoRetries: 1 #同一台服务器上的最大重试次数
  MaxAutoRetriesNextServer: 2 #要重试的下一个服务器的最大数量
  retryableStatusCodes: 500 #根据返回的状态码判断是否重试
  #是否对所有请求进行失败重试
  OkToRetryOnAllOperations: false #只对Get请求进行重试
  #OkToRetryOnAllOperations: true #对所有请求进行重试
注意:
对接口进行重试时,必须考虑具体请求方式和是否保证了幂等;如果接口没有保证幂等性(GET请求天然幂等),那么重试Post请求(新增操作),就有可能出现重复添加

7、总结

从手动重试,到使用Spring AOP自己动手实现,再到站在巨人肩上使用特别优秀的开源实现Spring Retry和Google guava-retrying,经过对各种重试实现方式的介绍,可以看到以上几种方式基本上已经满足大部分场景的需要:

  • 如果是基于Spring的项目,使用Spring Retry的注解方式已经可以解决大部分问题
  • 如果项目没有使用Spring相关框架,则适合使用Google guava-retrying:自成体系,使用起来更加灵活强大
  • 如果采用微服务架构开发,那直接使用Feign+Ribbon组件提供的重试即可

相关推荐

嵌入式C语言中常量的应用实例(嵌入式c语言中常量的应用实例是什么)

常量,我们都知道,就是数值保持不变的量。在C语言中,常量一旦初始化了,它的值将在整个程序运行周期内,不允许发生任何变化。常量与变量是相对的,我们实际项目中经常会用到它。定义常量的两种方式C语言中主要有...

C语言编程基础知识汇总学习,适合初学者!更新常量知识

(二)整型常量整型常量有3种形式:十进制整型常量、八进制整型常量和十六进制整型常量。(注意:c语言中没有直接表示二进制的整型常量,在c语言源程序中不会出现二进制。)书写方式如下:十进制整型常量:123...

【C语言】第二章第六节:字符串常量

第二章第六节:字符串常量。下表C语言中的常用转义字符。·字符形式功能:ASCIl码(十进制形式)。→\t水平制表(横向跳格:跳到下一个tab位置)。→\b退格8。→\r回车(不换行,光标移到本行行首)...

「GCTT 出品」Go 系列教程——5. 常量

这是我们Golang系列教程的第五篇。定义在Go语言中,术语”常量”用于表示固定的值。比如5、-89、IloveGo、67.89等等。看看下面的代码:varaint=50v...

每日C语言-常量指针、指针常量、指向常量的指针常量

一、常量指针1)什么是常量指针?通过该指针不可以修改其所指向存储单元中的值指针本身即地址可以被修改2)定义:类型说明符const*指针变量;类型说明符表示指针所指向存储单元中的值得数据类型指针...

C语言-符号常量、常变量、变量之我见

更新内容:新增音频。音频和文章一起更配oHello,大家好,又和大家见面了~~相信很多朋友们听了C语言的“符号常量”、“常变量”、“变量”后还是对这三者一脸懵逼吧。不管老师怎么歇斯底里地讲解,同学们迷...

零基础带你学习C语言:四:探索常量与变量

前言常量与变量学习;一:分析:short、float、long类型#include<stdio.h>intmain(){shortage=18;floatweight=12...

C语言中是如何定义常量的?那定义字符串呢?

常量有整型常量、浮点型常量、字符型常量及字符串常量。‘常量定义是指定义符号常量,用一个标识符来代表一个常量,通过宏定义预处理指令来实现。常量的定义:#definecount60这就定义了一个常量...

C语言符号常量的优点,会是那几点?

符号常量是一个常量,是不变量,所以,在编译的时候,就把符号常量出现的地方,替换为符号常量对应的常量。符号常量一般用户定义一个全局使用的数据,而且要改变该数据的时候,只需要改变符号常量的值,代码中引用符...

嵌入式开发- C语言数据类型-常量(c语言嵌入式是干嘛的)

基本数据类型的常量-掌握**整型常量:**常量是指在程序运行期间其数值不发生变化的数据。整型常量通常简称为整数整数可以是十进制数、八进制数、十六进制数八进制06334十六进制0xd1...

c语言解剖课:只读变量、常量、字面量傻傻分不清?

写在前面本篇主题的缘起,是因为一个计算机专业的大学生在和我讨论c语言问题时,说const常量如何如何,我说变量被const修饰了,还是变量,不是“常量”。他给了我一个截图:他说大模型都是这样回答的,变...

C/C++编程笔记:C数组、字符串常量和指针!三分钟弄懂它

想弄懂C语言中数组和指针的关系吗?这篇文章就占据你三分钟时间,看完你肯定会有收获!数组数组声明为数据类型名称[constant-size],并将一个数据类型的一个或多个实例分组到一个可寻址的位...

C语言入门到精通【第008讲】——C语言常量

C语言常量常量是固定值,在程序执行期间不会改变。这些固定的值,又叫做字面量。常量可以是任何的基本数据类型,比如整数常量、浮点常量、字符常量,或字符串字面值,也有枚举常量。常量就像是常规的变量,只不过常...

这是C语言无法修改得东西,C语言基础教程之常量解析

常量是指程序在执行期间不会改变的固定值。这些固定值也称为文字。常量可以是任何基本数据类型,如整数常量,浮点常量,字符常量或字符串文字,还有枚举常量。常量被视为常规变量,除了它们的值在定义后无法修改。整...

C语言中的单精度、双精度、常量等都有什么意思?

刚接触C语言时,对于常量,变量,浮点,单精度,双精度等问题的理解,大都很模糊不清,其实在程序运行过程中,其值不能改变的量称为常量。如12、0、-3为整型常量,4.6、-1.23为实型常量,'a'、'...

取消回复欢迎 发表评论: