聊聊重试:Guava Retrying
重试的一些知识点及应用场景
最近在做某小程序电商项目支付功能时,微信支付某个接口可能偶尔抽风,需要重试,这种还不能离线重试(XXL-JOB),只能在发送异常的时刻,进行一定次数的重试,这种情况,只能考虑在内存做重试。看图,借用知乎(西召)的图展示如何重试
这时候,搜刮自己脑瓜+搜索大法,可以总结出一些重试的知识点及重试的库(Spring-Retry、Guava Retring)。所以,借着此等机会,就认真好好梳理一下这块的知识点,以便自己或需要的开发人员做参考。
如果是你碰到这种情况,会如何处理呢?当然,还用问,直接啪啪啪写个工具类,比如以下的版本(v1):
是不是很快,一个for循环+重试次数,再做个异常处理,就能完成该需求。然后,自己思考一下,总觉得有哪些地方不合理呢?对着屏幕,苦思冥想,突然一个妹子路过,灵光一闪,对啊,要是请求的时候频率过快,3次的重试也很快就消耗没了,这样的代码重复执行也没啥好效果,然后脑海想起了Thread.sleep方法,就继续噼噼啪啪按着键盘,改成以下的版本(v2):
这次就完美了,考虑到请求频率。
随着业务的扩大,发现其他的场景也需要这种,这个时候,很多地方都是这种结构的代码了,于是乎,你就要思考,是不是要重构以下,抽取一个通用的工具类出来,有了思路,想到了jdk的并发库的Callable接口,跟着就埋头干起来...经过一段时间之后....,完美写出V3版本:
看着这个版本,是不是可以满足大部分的需求了,将需要重试的方法,封装到Callable接口里,让其在try/catch中执行,如果有结果返回直接返回,异常之类的情况则重试,并且能根据业务的需求,设置睡眠时间。
当你完成以上杰作的时候,是不是感觉美滋滋的,达到了“人生巅峰”,不断傻笑欣赏着自己的杰作,然而直到你看到了神器“Guava Retrying”的登场,顿时泄气了,觉得自己以前写的什么玩意。除了实用,啥高大上的概念都没用上哈。然后你不服,只能埋头去研究guava retrying的实现。
在看GuavaRetrying的源码前,也得先恶补一下重试的一些知识点。
try-catch-redo简单重试模式
在包装正常上传逻辑基础上,通过判断返回结果或监听异常决定是否重试,同时为了解决立即重试的无效执行(假设异常是有外部执行不稳定导致的:网络抖动),休眠一定延迟时间后重新执行功能逻辑。
try-catch-redo-retry strategy策略重试模式
上述方案还是有可能重试无效,解决这个问题尝试增加重试次数retrycount以及重试间隔周期interval,达到增加重试有效的可能性。
方案一和方案二存在一个问题:正常逻辑和重试逻辑强耦合,重试逻辑非常依赖正常逻辑的执行结果,对正常逻辑预期结果被动重试触发,对于重试根源往往由于逻辑复杂被淹没,可能导致后续运维对于重试逻辑要解决什么问题产生不一致理解。重试正确性难保证而且不利于运维,原因是重试设计依赖正常逻辑异常或重试根源的臆测。
应用命令设计模式解耦正常和重试逻辑
就是利用jdk的callable之类的接口
一个完备的重试实现,要很好地解决如下问题:
l什么条件下重试
l什么条件下停止
l如何停止重试
l停止重试等待多久
l如何等待
l请求时间限制
l如何结束
l如何监听整个重试过程
并且,为了更好地封装性,重试的实现一般分为两步:
l使用工厂模式构造重试器
l执行重试方法并得到结果
一个完整的重试流程可以简单示意为:
好,带着这些问题,我们来开始我们的guava Retrying 库的源码分析之路
Guava Retrying 库的介绍
Guava Retrying是一个灵活方便的重试组件,包含了多种的重试策略,而且扩展起来非常容易。使用Guava-retrying你可以自定义来执行重试,同时也可以监控每次重试的结果和行为,最重要的基于 Guava 风格的重试方式真的很方便。
作者原话:
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.
Guava Retring的使用方式示例
引入Guava Retring库(maven)
定义实现Callable接口的方法,让Guava的Retryer类能够调用
定义Retry对象及设置相关策略
如此,通过以上三个简单步骤就能使用Guava Retrying实现重试。
那么接下来介绍一下API的简要说明
主要接口介绍
lAttempt:一次执行任务;
¡AttemptTimeLimiter:单次任务执行时间限制(如果单次任务执行超时,则终止执行当前任务);
¡ExceptionAttempt:执行异常
lBlockStrategies:BlockStrategy的工厂类,任务阻塞策略,确定重试器应如何在两次重试之间阻塞的策略(通俗的讲就是当前任务执行完,下次任务还没开始这段时间做什么),默认策略为:BlockStrategies.THREAD_SLEEP_STRATEGY 也就是调用 Thread.sleep(sleepTime),但是如果需要的话,实现可能会更加复杂。
lRetryException:重试异常;
lRetryListener:自定义重试监听器,可以用于异步记录错误日志;
lStopStrategy:停止重试策略,提供三种:
¡StopAfterDelayStrategy :设定一个最长允许的执行时间;比如设定最长执行10s,无论任务执行次数,只要重试的时候超出了最长时间,则任务终止,并返回重试异常RetryException;
¡NeverStopStrategy :不停止,用于需要一直轮训知道返回期望结果的情况;
¡StopAfterAttemptStrategy :设定最大重试次数,如果超出最大重试次数则停止重试,并返回重试异常;
lWaitStrategy:对应工厂类WaitStrategies,等待时长策略(控制时间间隔),返回结果为下次执行时长:
¡FixedWaitStrategy:固定等待时长策略;
¡RandomWaitStrategy:随机等待时长策略(可以提供一个最小和最大时长,等待时长为其区间随机值)
¡IncrementingWaitStrategy:递增等待时长策略(提供一个初始值和步长,等待时间随重试次数增加而增加)
¡ExponentialWaitStrategy:指数等待时长策略;
¡FibonacciWaitStrategy :Fibonacci 等待时长策略;
¡ExceptionWaitStrategy :异常时长等待策略;
¡CompositeWaitStrategy :复合时长等待策略;
Guava Retring 项目源码结构
(源码类,从这里基本可以看到它的实现非常简洁)
我们在Ideal利用【Diagram】工具,可以导出比较详细的源码依赖图,如下(可能比较大,需要下载观看)
下面我会根据该图中展示的关系,逐一解读。
你可以看到这个库的代码层次划分清晰,合理实用工厂、建造器和策略等设计模式,把重试的方方面面安排的明明白白。
Guava Retrying 源码:RetryerBuilder
重试器构造类,看如下代码截图
Guava Retrying 源码:Retryer
重试器,担当所有任务执行、任务重试策略、任务终止策略等流程执行。看改类一部分代码截图:
该类(Guava Retrying库)的核心方法,如下:
Guava Retrying 源码:WaitStrategies与WaitStrategy
WaitStrategy,等待策略接口,里面只要一个方法:
long computeSleepTime(Attempt failedAttempt); 返回下一次重试所需要等待的时间。传入的是一次执行失败的任务。
WaitStrategies,策略生成接口,依赖WaitStrategy,根据实际需求,生成所需要的等待策略
我们可以随便看一个具体策略实现,如下截图
Guava Retrying 源码:StopStrategy与AttemptTimeLimiters
StopStrategy:停止重试策略,StopStrategies是停止策略生成工厂。
StopStrategies:停止重试策略工厂类,里面包含了StopStrategy实现内部类;
AttemptTimeLimiters:将任何一个执行任务尝试包装在时间限制内的规则,如果超过该时间限制,则可能会被中断。
后记
Guava Retrying是基于内存、Callable、Thread.sleep等纯JDK的综合应用库,但实际的项目开发中,内存中重试往往也只是一种场景,更多需要离线、非实时这种重试,那么如果基于Guava Retrying你能有多少脑洞来做自己的重试机制,比如,结合Redis、开源的分布式定时调度框架(xxljob)、RabbitMQ队列服务(延迟重试)等等,想象一下,是不是突破了单机的限制。
感谢以下文章:
https://my.oschina.net/u/4278661/blog/4163080
https://juejin.im/post/6844903785031008263
https://juejin.im/post/6844903617183350798
总结
读代码是一种学习编码技巧和设计技巧的捷径,但读过了再写出属于自己对其中的理解就是一种升华。
通过这一次对Guava Retrying库的学习,可以感受到为了达到优雅,达到各种场景的使用所付出的努力,当然,在具体项目编码中,你可以简单使用一个工具类来做到类似的工作,但这不应该是一个库,想成为一个各种场景下都能无缝使用的库应该这样做的。所以需要运用库作者高超的编码技巧、深厚的OOP理论基础和对设计模式的灵活使用。
由于技术的发展,都说Java这一行容易“混”起来了,但“混”起来容易,有追求的开发应该要时刻要升华自己,多向前辈学习,不单单停留在CRUD的围城里。