Spring Boot教程(27) – 优雅地实现重试逻辑

Spring家族中有一个小项目是Spring Retry,他用来方便地实现重试机制。你的程序在调用远程API的时候,可能因为网络抖动等原因,导致调用失败,这种失败是偶发的,出现了多试几下就行。如果你遇到需要重试多次的情况,倘若只是朴素地写个for循环N次,请求成功就跳出循环返回结果,未免也太粗糙了。Spring已经为这一通用需求造好了轮子。

Spring Retry现在被Spring BatchSpring Integration等项目引用,在Spring Boot项目中,我一般直接引入spring-boot-start-batch来间接引入Spring Retry(Spring Batch是个批处理工具库,你也许会用得到其中的类,我们有机会再具体研究下),另外它还要配合AOP Starter才能使用,后者你的项目里一般都有。引入Spring Retry后,可以用加注解声明的方式,或者编码的方式来使用。

先来看看最简单的使用注解:

开启功能
编写业务逻辑

Spring Retry使用了Spring AOP,一旦通过@EnableRetry开启重试的功能,那么@Retryable注解对应的方法就会被包装起来。就像上图中的yourMethod方法,如果执行的过程中发生了异常,那么yourMethod就会再次执行,直到次数到了注解参数maxAttempts指定的最大值,如果所有尝试都不成功,异常就会抛出。@Retryable注解还有很多其他参数,比如include参数可以指定发生哪些异常会重试,比如exclude参数表示哪些异常不重试,还有backoff参数可以指定一次失败之后,过多长时间再开始下一次重试。

由于Spring AOP的缺陷,你不能在上图中DemoService的内部调用yourMethod方法,否则会失去重试的效果。你只能在DemoService外部调用,或者使用比较tricky的方法来绕过。具体原因可以查看我之前写的讲解AOP的文章

注解声明式的使用,最终执行的时候也是使用了一套基本的编程API,我们简单看一下如何直接使用这些API:

上图的代码中,我们创建了一个RetryTemplate对象,这个对象是重试相关操作的发起者。接下来设置了他的RetryPolicyBackOffPolicy,这两个接口,前者定义了什么情况下开始重试,什么情况下停止重试,后者定义了两次重试的时间间隔应该如何确定。

RetryTemplate中默认的RetryPolicySimpleRetryPolicy,它限制尝试的次数,最大尝试次数是3。如果你想自定义,可以像上图那样自己创建一个SimpleRetryPolicy对象,用它设置你想要的最大重试次数,和遇到哪些异常才重试。我建议你显式指定需要重试的异常,比如IO异常,这样可以避免某些情况下进行无谓的重试,浪费时间。

RetryTemplate中默认的BackOffPolicyNoBackOffPolicy,意思就是一旦失败了,就立马重试。如果你觉的需要定义间隔时间,可以使用如下几个:

  • FixedBackOffPolicy 设定固定的间隔时间
  • UniformRandomBackOffPolicy 在一个时间区间内[min,max]随机出一个时间间隔
  • ExponentialBackOffPolicy 让间隔时间按照某种比例增长,比如第一次隔1秒,第二次隔2秒,第三次隔4秒,依此类推。setInitialInterval方法设置初始的间隔时间,setMultiplier设置时间的增长速率,默认是2,意味着每次时间翻倍。你也可以给间隔时间设置一个上限,时间过长

说完了RetryPolicyBackOffPolicy,我们再来看看RetryTemplate的核心方法execute,他的参数是一个RetryCallback对象,其中doWithRetry方法里运行的就是你的业务逻辑,你可以将单独的业务逻辑提取出来,在doWithRetry中调用。代码如下,businessLogic方法独立出来后可以在其他地方复用,execute方法调用显得更简洁,且返回值result就是businessLogic方法执行成功时候的返回值:

doWithRetry方法有个RetryContext类型的参数context,它包含了此次重试的信息,比如当前是第几次重试,上次发生的异常等等。一般情况下你不会用到它。

RetryTemplate的execute方法还有另外一种重载形式,多了一个RecoveryCallback对象。它是在重试次数用完的时候调用的,默认情况下用完次数会抛出异常,但是你可能懒得去处理,可以用RecoveryCallback,在所有重试都失败的时候提供个返回值,可以是默认值或者是从其他什么地方拿来的值。

不管是使用注解声明还是使用RetryTemplate,他们的效果其实等价的,后者可能更灵活一点,我经常在项目中选用。如果你想看注解是如何被转换成RetryTemplate调用的,可以查看AnnotationAwareRetryOperationsInterceptor类。

Spring Retry的内容还是挺简单的,Spring生态中有很多这种使常见代码模型简化的工具,如果你都掌握的很好,可以使你的代码显得更健壮更易读。

关于 “Spring Boot教程(27) – 优雅地实现重试逻辑” 的 2 个意见

  1. 重试的问题一般还是非常复杂的,系统很容易因为 重试导致雪崩。

    特别是调用链很长的系统,一个极小的下游故障,很有可能会引起层层重试,加上上下游不合理的超时时间,导致及其严重的问题。

发表评论

电子邮件地址不会被公开。