java基础知识代码规范 重试实现高可用一览
java基础知识代码规范 重试实现高可用一览
2024-11-22 10:29:57  作者:生活如  网址:https://m.xinb2b.cn/tech/qfu149559.html
1、背景介绍

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

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

2、测试环境2.1 模拟远程调用

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

@Slf4j@Servicepublic class RemoteService { private final static AtomicLong count = new AtomicLong(0); public String hello() { long current = count.incrementAndGet(); System.out.println("第" current "次被调用"); if (current % 3 != 0) { log.warn("调用失败"); return "error"; } return "success"; }}

2.2 单元测试

编写单元测试:

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

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


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

@Testpublic 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)@Documentedpublic @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@Slf4jpublic 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)编写单元测试

@Testpublic 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@EnableRetrypublic class RemoteApplication { public static void main(String[] args) { SpringApplication.run(RemoteApplication.class); }}

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

@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)编写单元测试,验证效果

@Testpublic 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接口的都是)

@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";}

编写单元测试验证:

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


4.2.3 指定熔断机制

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

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

@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";}@Recoverpublic 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;)

@Retryablepublic String hello3() { long current = count.incrementAndGet(); System.out.println("第" current "次被调用"); if (current % 3 != 0) { log.warn("调用失败"); return "error"; } return "success";}

2、因此就算在方法上添加@Retryable,也无法实现失败重试

编写单元测试:

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

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


5、另一个巨人谷歌 guava-retrying5.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中的注解

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()

@Testpublic 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还有很多更灵活的配置和使用方式:

通过retryIfException 和 retryIfResult 来判断什么时候进行重试,同时支持多个且能兼容。设置重试监听器RetryListener,可以指定发生重试后,做一些日志记录或其他操作.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组件提供的重试即可,
  • 三月自驾游赤壁(旅行日志之赤壁)
  • 2024-11-22旅行日志之赤壁苏轼〔宋代〕的念奴娇·赤壁怀古道出了人们对昔日英雄人物的敬仰之情如今的赤壁焕发出勃勃生机,已成为一个新兴的集旅游与茶文化为一体的现代化城市第一天我是第一次来赤壁,我要好好看看这个曾经的战场是什么样子,。
  • 红小豆正确栽培技术(红小豆栽培方法)
  • 2024-11-22红小豆栽培方法选良种一般来说,农民朋友会自留种子,也有到市场上购买的,一定要选择纯净度高、籽粒饱满度高、籽粒光泽度好的红小豆品种,避免选择发霉、破粒、瘪粒的品种,而且要根据种植当地的环境选择抗病品种,这样对高产有很。
  • 长得和萨摩耶差不多的小狗(比小奶狗酷)
  • 2024-11-22比小奶狗酷前几年,人人喊着上瘾成熟有型的“雅痞”风格如今犬系男势头暴涨,姑娘们迷恋小奶狗与小狼狗之间的自由切换李泽言、许墨、白起、周棋洛这四个名字也制霸了社交网络女孩们不追剧了,而是沉迷于《恋与制作人》这款游戏。
  • win10无法装cad怎么回事(win10安装不了cad怎么办)
  • 2024-11-22win10安装不了cad怎么办天正CAD林老师给大家带相关的方法一览,感兴趣的朋友不要错过了哦,希望这篇文章能对大家有所帮助1、第1步首先检查是不是自己下载的cad软件的版本不对,跟win10系统不兼容导致比如说在win1032位。
  • 我们仨经典语录(我们仨经典语录有哪些)
  • 2024-11-22我们仨经典语录有哪些我一个人,怀念我们仨现在我们三个失散了往者不可留,逝者不可追;剩下的这个我,再也找不到他们了我大声呼喊,连名带姓地喊喊声落在旷野里,好像给吞吃了似的,没留下一点依稀仿佛的音响彻底的寂静,给沉沉夜色增添。
  • 唐朝韦后与武则天(大唐三百年的八位皇后)
  • 2024-11-22大唐三百年的八位皇后大唐三百年,四分之三的岁月没有皇后仅有的八位皇后,正式在位时间七十多年,其中武后一人独占28年唐朝后期几乎没有皇后,隔了一百年重新册立的皇后就是末代皇后了除了唐太宗长孙皇后和当了女帝的唐高宗武后,剩下。
  • dnf圣职者国风装扮(DNF圣职者cos卢克爷爷)
  • 2024-11-22DNF圣职者cos卢克爷爷最近出现一波市长cos热潮尤其是大叔cos卢克爷爷的时装大火啊,下面就来看看这些很有趣的时装搭配吧!圣职者cos卢克爷,首先是商城能买的几件:接下来的几件,尤其是下装需要夏日套的其他随便配配就很像了啊。
  • 胡歌演的哪些电视剧最好看(胡歌的这5部电视剧)
  • 2024-11-22胡歌的这5部电视剧小编一直觉得胡歌是娱乐圈中的一股清流,不靠炒作,没有绯闻,只是踏踏实实的演戏,努力做好一位演员该做的事情,并且品德还好,致力于公益事业,简直是一位十全十美的偶像1《仙剑奇侠传》是胡歌主演的一部古装仙侠。
  • 正宗剁椒金针菇做法(2022美食创新)
  • 2024-11-222022美食创新一提到食材剁椒,相信大部分人都会只会制作鱼头,实际下食材剁椒是用小米椒做成,味辣而鲜咸,很多四川、湖南、湖北等省的特色食品都会放置,今天不一样,我为大家来一天个创新食材制作法!剁椒这样做法更好吃湘西人。
  • 怎么看懂儿童画 如何看懂儿童画-1
  • 2024-11-22怎么看懂儿童画 如何看懂儿童画-1点击“蓝字”关注我们如何看懂儿童画AUTUMNTIME绘画是多彩童年的重要元素之一,也是儿童表达观点、想法、情绪情感等的重要方式儿童会用手中的画笔,画出他所观察到的五彩世界但拿起儿童的绘画作品,相信很。
  • 王者荣耀账号价值已出(账号价值105万你能信)
  • 2024-11-22账号价值105万你能信Hello,大家好,这里是头号游戏,每天都会带来最新的游戏资讯!在王者荣耀这款游戏中,每天活跃的玩家都已经达到了上千万,并且这些玩家个个都是不一样的,王者荣耀能够保持这样的热度,也是因为独特的运营方式。
  • 澳大利亚的性侵事件 一男子在布碌仑性侵一13岁少年
  • 2024-11-22澳大利亚的性侵事件 一男子在布碌仑性侵一13岁少年【侨报网报道】纽约警方周四表示,周一,一名男子在布碌仑性侵了一名13岁男孩根据pix11报道,周一下午4点左右,嫌疑人在洛克威大道(RockawayParkway)和温思罗普街(WinthropStr。