SpringBoot重试策略Retry
一、简介
1、重试机制
重试机制在网络服务中非常的重要,由于网路可能存在延迟,网络抖动,网络不稳定的情况。同时在分布式服务中网络的请求的高度密集,有些服务不一定能在规定的时间内完成访问。应该请求服务需要重试几次。以保证服务请求成功。
例如对接支付接口时,因为回调比较重要,当访问失败时会进行重试,不过此时的重试机制时间是逐步加大,例如30s/1m/10m/1h等,最终到达阈值不在重试
对于重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。比如外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。
2、重试机制设计、共性和原理
-
无侵入:这个好理解,不改动当前的业务逻辑,对于需要重试的地方,可以很简单的实现
-
可配置:包括重试次数,重试的间隔时间,是否使用异步方式等
-
通用性:最好是无改动(或者很小改动)的支持绝大部分的场景,拿过来直接可用
-
正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介
-
约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性
-
都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑
-
Spring-Retry
和Guava-Retryer
工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性
3、硬编码重试
本文会详细介绍Spring-Retryr
和Guava-Retry
两个重试组件,再次之前先看一下硬编码重试方法
1 | 4j |
二、重试框架之Spring-Retry
1、介绍
Spring Retry 为 Spring 应用程序提供了声明性重试支持。它用于Spring批处理、Spring集成、Apache Hadoop(等等)。它主要是针对可能抛出异常的一些调用操作,进行有策略的重试
环境搭建首先进行pom.xml
进入
1 | <dependency> |
2、Spring-Retry的普通使用方式
2.1 Demo搭建
准备一个任务方法,我这里是采用一个随机整数,根据不同的条件返回不同的值,或者抛出异常
1 | 4j |
业务重试代码
1 | 4j |
-
RetryTemplate
承担了重试执行者的角色,它可以设置SimpleRetryPolicy
(重试策略,设置重试上限,重试的根源实体),FixedBackOffPolicy
(固定的回退策略,设置执行重试回退的时间间隔)。 -
RetryTemplate
通过execute
提交执行操作,需要准备RetryCallback
和RecoveryCallback
两个类实例,前者对应的就是重试回调逻辑实例,包装正常的功能操作,RecoveryCallback
实现的是整个执行操作结束的恢复操作实例 -
只有在调用的时候抛出了异常,并且异常是在
exceptionMap
中配置的异常,才会执行重试操作,否则就调用到excute
方法的第二个执行方法RecoveryCallback
中
2.2 重试策略
-
NeverRetryPolicy: 只允许调用
RetryCallback
一次,不允许重试 -
AlwaysRetryPolicy: 允许无限重试,直到成功,此方式逻辑不当会导致死循环
-
SimpleRetryPolicy: 固定次数重试策略,默认重试最大次数为3次,
RetryTemplate
默认使用的策略 -
TimeoutRetryPolicy: 超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
-
ExceptionClassifierRetryPolicy: 设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
-
CircuitBreakerRetryPolicy: 有熔断功能的重试策略,需设置3个参数
openTimeout
、resetTimeout
和delegate
-
CompositeRetryPolicy: 组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许即可以重试,悲观组合重试策略是指只要有一个策略不允许即可以重试,但不管哪种组合方式,组合中的每一个策略都会执行
2.3 重试回退策略
重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy
。
-
NoBackOffPolicy: 无退避算法策略,每次重试时立即重试
-
FixedBackOffPolicy: 固定时间的退避策略,需设置参数
sleeper
和backOffPeriod
,sleeper
指定等待策略,默认是Thread.sleep
,即线程休眠,backOffPeriod
指定休眠时间,默认1秒 -
UniformRandomBackOffPolicy: 随机时间退避策略,需设置
sleeper
、minBackOffPeriod
和maxBackOffPeriod
,该策略在minBackOffPeriod
,maxBackOffPeriod
之间取一个随机休眠时间,minBackOffPeriod
默认500毫秒,maxBackOffPeriod
默认1500毫秒 -
ExponentialBackOffPolicy: 指数退避策略,需设置参数
sleeper
、initialInterval
、maxInterval
和multiplie
r,initialInterval
指定初始休眠时间,默认100毫秒,maxInterval
指定最大休眠时间,默认30秒,multiplier
指定乘数,即下一次休眠时间为当前休眠时间*multiplier
-
ExponentialRandomBackOffPolicy: 随机指数退避策略,引入随机乘数可以实现随机乘数回退
我们可以根据自己的应用场景和需求,使用不同的策略,不过一般使用默认的就足够了。
2.4 其他扩展
配置重试策略和退避策略
1 |
|
测试
1 |
|
3、Spring-Retry注解式(推荐)
3.1 注解介绍
下面注解方法为常用方法,具体可以自己探索
@EnableRetry
表示是否开始重试组件
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
proxyTargetClass | boolean | false | 指示是否要创建基于子类的(CGLIB)代理,而不是创建标准的基于Java接口的代理 |
@Retryable
标注此注解的方法在发送异常时会进行重试
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
interceptor | String | “” | 将interceptor的bean名称应用到retryable(),和其他的属性互斥 |
include | Class[] | {} | 哪些异常可以触发重试 ,默认为空 |
exclude | Class[] | {} | 哪些异常将不会触发重试,默认为空,如果和include属性同时为空,则所有的异常都将会触发重试 |
value | Class[] | {} | 可重试的异常类型 |
label | String | “” | 统计报告的唯—标签。如果没有提供,调用者可以选择忽略它,或者提供默认值 |
maxAttempts | int | 3 | 尝试的最大次数(包括第一次失败),默认为3次 |
backoff | @Backoff | @Backoff() | @Backoff @Backoff()指定用于重试此操作的backoff属性。默认为空 |
@Backoff
属性 | 类型 | 默认值 | 说明 |
---|---|---|---|
delay | long | 0 | 如果不设置则默认使用1000 ms等待重试,和value同义词 |
maxDelay | long | 0 | 最大重试等待时间 |
multiplier | long | 0 | 用于计算下一个延迟延迟的乘数(大于0生效) |
random | boolean | false | 随机重试等待时间 |
3.2 注解式实战
因为注解需要用到切面,所以需要引入依赖
1 | <dependency> |
配置注解式重试方法
1 |
|
进行测试,发现可以成功重试
1 |
|
4、监听重试过程
4.1 简介
-
通过实现RetryListener接口,重写
open、close、onError
这三个方法,既可以完成对重试过程的追踪,也可以添加额外的处理逻辑; -
通过继承RetryListenerSupport,也可以从
open、close、onError
这三个方法中,选择性的重写
普通方式使用时(注解方式不需要),在实例化RetryTemplate时,配置上该RetryListener实例即可:retryTemplate.setListeners(new RetryListener[] {retryListenerTemplate});
另外每个RetryTemplate可以注册多个监听器,其中onOpen、onClose方法按照注册顺序执行,onError按照注册顺序的相反顺序执行
4.2 实现RetryListener接口
1 | 4j |
4.3 继承RetryListenerSupport
1 |
|
三、重试框架之Guava-Retrying
1、介绍
Guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。
Guava Retryer也是线程安全的,入口调用逻辑采用的是Java.util.concurrent.Callable
的call方法
首先需要引入依赖
1 | <dependency> |
2、Guava-Retrying普通使用方式(官方)
2.1 Demo实战
首先创建一个服务类
1 | 4j |
测试类
1 |
|
2.2 重试机制
RetryerBuilder的retryIfXXX()方法用来设置在什么情况下进行重试,总体上可以分为根据执行异常进行重试和根据方法执行结果进行重试两类。
方法 | 描述 |
---|---|
retryIfException() | 抛出 runtime 异常、checked 异常时都会重试,但是抛出 error 不会重试 |
retryIfRuntimeException() | 会在抛 runtime 异常的时候才重试,checked 异常和error 都不重试 |
retryIfException(Predicate exceptionPredicate) | 这里当发生异常时,会将异常传递给exceptionPredicate,那我们就可以通过传入的异常进行更加自定义的方式来决定什么时候进行重试 |
retryIfExceptionOfType(Class<? extends Throwable> exceptionClass) | 许我们只在发生特定异常的时候才重试,比如NullPointerException 和 IllegalStateException 都属于 runtime 异常,也包括自定义的error |
retryIfResult(@Nonnull Predicate resultPredicate) | 传入的resultPredicate返回true时则进行重试 |
2.3 停止重试相关策略
StopStrategy
停止重试策略用来决定什么时候不进行重试,其接口com.github.rholder.retry.StopStrategy,停止重试策略的实现类均在com.github.rholder.retry.StopStrategies中,它是一个策略工厂类
-
NeverStopStrategy:此策略将永远重试,永不停止
-
StopAfterAttemptStrategy:当执行次数到达指定次数之后停止重试
-
StopAfterDelayStrategy:当距离方法的第一次执行超出了指定的delay时间时停止,也就是说一直进行重试,当进行下一次重试的时候会判断从第一次执行到现在的所消耗的时间是否超过了这里指定的delay时间,查看其实现
WaitStrategy
-
IncrementingWaitStrategy:在决定任务间隔时间时,返回的是一个递增的间隔时间,即每次任务重试间隔时间逐步递增,越来越长
-
RandomWaitStrategy:返回一个随机的间隔时长,我们需要传入的就是一个最小间隔和最大间隔,然后随机返回介于两者之间的一个间隔时长
-
FixedWaitStrategy:返回一个固定时长的重试间隔
-
ExceptionWaitStrategy:由方法执行异常来决定是否重试任务之间进行间隔等待,以及间隔多久
-
FibonacciWaitStrategy:与IncrementingWaitStrategy有点相似,间隔时间都是随着重试次数的增加而递增的,不同的是,FibonacciWaitStrategy是按照斐波那契数列来进行计算的,使用这个策略时,我们需要传入一个乘数因子和最大间隔时长
-
ExponentialWaitStrategy:与IncrementingWaitStrategy、FibonacciWaitStrategy也类似,间隔时间都是随着重试次数的增加而递增的,但是该策略的递增是呈指数级递增
-
WaitStrategy:随机时间间隔以及不等待
RetryListener
当发生重试时,将会调用RetryListener的onRetry方法,此时我们可以进行比如记录日志等额外操作
3、Guava-Retrying注解式(非官方)
因为注解需要用到切面,所以需要引入依赖
1 | <dependency> |
创建自定义注解
1 | (ElementType.METHOD) |
创建AOP切面方法增强
1 |
|
创建方法
1 |
|
最后测试
1 |
|
四、源码简析
1、Spring-Retry源码简析
2、Guava-Retrying源码简析
实现原理大概就是由上述各种策略配合从而达到了非常灵活的重试机制
1 | public interface Attempt<V> { |
通过接口方法可以知道Attempt这个类包含了任务执行次数、任务执行异常、任务执行结果、以及首次执行任务至今的时间间隔,那么我们后续的不管重试时机、还是其他策略都是根据此值来决定。接下来看关键执行入口Retryer##call
:
1 | public V call(Callable<V> callable) throws ExecutionException, RetryException { |
参考文章: