重试机制的业务背景
外部服务对于调用者来说一般都是不可靠的,尤其是在网络环境比较差的情况下,网络抖动很容易导致请求超时等异常情况,这时候就需要用失败重试策略重新调用 API 接口来获取。
在分布式系统中,为了保证数据分布式事务的强一致性,大家在调用RPC接口或者发送MQ时,针对可能会出现网络抖动请求超时情况采取一下重试操作。 大家用的最多的重试方式就是MQ了,但是如果你的项目中没有引入MQ,那就不方便了。
重试策略的介绍和限制
重试策略在服务治理方面也有很广泛的使用,通过定时检测,来查看服务是否存活。
重试是有场景限制的,不是什么场景都适合重试,比如参数校验不合法、写操作等(要考虑写是否幂等)都不适合重试。
- 远程调用超时、网络突然中断可以重试。在微服务治理框架中,通常都有自己的重试与超时配置,比如dubbo可以设置retries=1,timeout=500调用失败只重试1次,超过500ms调用仍未返回则调用失败。
重试的场景有哪些?
外部 RPC 调用,或者数据入库等操作,如果一次操作失败,可以进行多次重试,提高调用成功的可能性。
常用的重试框架
Spring异常重试框架Spring Retry:Spring Retry支持集成到Spring或者Spring Boot项目中,而它支持AOP的切面注入写法,所以在引入时必须引入aspectjweaver.jar包。
sisyphus 综合了 spring-retry 和 gauva-retrying 的优势,使用起来也非常灵活。
guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。
spring-retry的重试机制
Spring Retry 为 Spring 应用程序提供了声明性重试支持。 它用于Spring批处理、Spring集成、Apache Hadoop(等等)的Spring。
This is a small extension to Google’s Guava library to allow for the creation of configurable retrying strategies for an arbitrary function call, such as something that talks to a remote service with flaky uptime.
Maven配置
启用重试功能
启动类上面添加@EnableRetry注解,启用重试功能,或者在使用retry的service上面添加也可以,或者Configuration配置类上面。建议所有的Enable配置加在启动类上,可以清晰地统一管理使用的功能。
添加@Retryable和@Recover注解
@Retryable注解,被注解的方法发生异常时会重试
- value:指定发生的异常进行重试
- include:和value一样,默认空,当exclude也为空时,所有异常都重试
- exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试
- maxAttemps:重试次数,默认3
- backoff:重试补偿机制,默认没有
@Backoff注解
delay:指定延迟后重试
multiplier:指定延迟的倍数,比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二 次为10秒,第三次为20秒
@Recover注解
当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。
guava-retry
guava-retrying 模块提供了一种通用方法, 可以使用Guava谓词匹配增强的特定停止、重试和异常处理功能来重试任意Java代码。
guava-retry的Git地址
https://github.com/rholder/guava-retrying
优势
guava retryer工具与spring-retry类似,都是通过定义重试者角色来包装正常逻辑重试,但是Guava retryer有更优的策略定义,在支持重试次数和重试频度控制基础上,能够兼容支持多个异常或者自定义实体对象的重试源定义,让重试功能有更多的灵活性。
Guava Retryer也是线程安全的,入口调用逻辑采用的是 java.util.concurrent.Callable 的 call() 方法,遇到异常之后,重试 3 次停止
public static void main(String[] args) {
Callable<Boolean> callable = new Callable<Boolean>() {
@Override
public Boolean call() throws Exception {
// do something useful here
LOGGER.info("call...");
throw new RuntimeException();
}
};
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder()
.retryIfResult(Predicates.isNull())
.retryIfExceptionOfType(IOException.class)
.retryIfRuntimeException()
.withStopStrategy(StopStrategies.stopAfterAttempt(3))
.build();
try {
retryer.call(callable);
} catch (RetryException | ExecutionException e) {
e.printStackTrace();
}
}
其主要接口及策略介绍
- Attempt:一次执行任务;
- AttemptTimeLimiter:单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);
- BlockStrategies:任务阻塞策略(通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么……- - BlockStrategies.THREAD_SLEEP_STRATEGY 也就是调用 Thread.sleep(sleepTime);
- RetryException:重试异常;
- RetryListener:自定义重试监听器,可以用于异步记录错误日志;
- StopStrategy:停止重试策略,提供三种:
- StopAfterDelayStrategy:设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
- NeverStopStrategy:不停止,用于需要一直轮训直到返回期望结果的情况;
- StopAfterAttemptStrategy:设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;
- WaitStrategy:等待时长策略(控制时间间隔),返回结果为下次执行时长:
- FixedWaitStrategy:固定等待时长策略;
- RandomWaitStrategy:随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)
- IncrementingWaitStrategy:递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
- ExponentialWaitStrategy:指数等待时长策略;
- FibonacciWaitStrategy :Fibonacci 等待时长策略;
- ExceptionWaitStrategy :异常时长等待策略;
- CompositeWaitStrategy :复合时长等待策略;
根据结果判断是否重试
使用场景:如果返回值决定是否要重试。
重试接口
重试策略设定无限重试
使用场景:在有异常情况下,无限重试(默认执行策略),直到返回正常有效结果;
根据异常判断是否重试
使用场景:根据抛出异常类型判断是否执行重试。
等待策略——设定重试等待固定时长策略
使用场景:设定每次重试等待间隔固定为10s;
等待策略——设定重试等待时长固定增长策略
场景:设定初始等待时长值,并设定固定增长步长,但不设定最大等待时长;
例如:调用间隔时间递增1秒:
重试框架的总结
优雅重试共性和原理
正常和重试优雅解耦,重试断言条件实例或逻辑异常实例是两者沟通的媒介,还有一种方式,是开发者自己编写重试机制,但是大多不够优雅
约定重试间隔,差异性重试策略,设置重试超时时间,进一步保证重试有效性以及重试流程稳定性。
都使用了命令设计模式,通过委托重试对象完成相应的逻辑操作,同时内部封装实现重试逻辑。
spring-retry 和 guava-retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。两种方式都是比较优雅的重试策略,Spring-retry配置更简单,实现的功能也相对简单,Guava本身就是谷歌推出的精品java类库,guava-retry也是功能非常强大,相比较于Spring-Retry在是否重试的判断条件上有更多的选择性,可以作为Spring-retry的补充。
优雅重试适用场景
功能逻辑中存在不稳定依赖场景,需要使用重试获取预期结果或者尝试重新执行逻辑不立即结束。比如远程接口访问,数据加载访问,数据上传校验等等。
对于异常场景存在需要重试场景,同时希望把正常逻辑和重试逻辑解耦。
对于需要基于数据媒介交互,希望通过重试轮询检测执行逻辑场景也可以考虑重试方案。