Spring-retry重试机制

在调用第三方接口或服务时,会出现网络抖动,连接超时等网络异常,所以实际工作中会需要重试的功能。比如取消第三方的订单或者同步订单给第三方,可能网络故障第一次没有操作成功,后续的重试操作,可以帮助取消和同步的操作最终能够执行成功。在Spring全家桶里面Spring Retry则是提供重试和熔断(停止重试)功能的模块,此外,在spring retry中可以指定需要重试的异常类型,并设置每次重试的间隔。

使用示例

本节示例代码基于SpringBoot,版本是2.1.5.RELEASE。完整代码请参考我的GitHub
spring retry模块需要引入对应的依赖:

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.retry</groupId>
<artifactId>spring-retry</artifactId>
<version>1.2.4.RELEASE</version>
</dependency>
<!-- Spring Retry使用了AOP -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>

启动类:

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
public class RetryBootStrap {

public static void main(String[] args) {
new SpringApplicationBuilder(RetryBootStrap.class)
.registerShutdownHook(true)
.run(args);
}

}

重试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
@Service
@Slf4j
public class RetryService implements InitializingBean {


private static RetryTemplate retryTemplate;



@Override
public void afterPropertiesSet() {
retryTemplate = new RetryTemplate();
retryTemplate.setRetryPolicy(new SimpleRetryPolicy(5)); //重试5次
FixedBackOffPolicy fbop = new FixedBackOffPolicy();
fbop.setBackOffPeriod(300); //重试间隔300毫秒
retryTemplate.setBackOffPolicy(fbop);
}


/**
* 重试取消订单
*
* @param orderId
*/
public void cancelThirdOrder(long orderId) {
final RetryCallback<Void, Throwable> retry = context -> {
log.info("重试取消第三方订单|START|orderId={}", orderId);
int i = new Random().nextInt(10);
if (i != 5) {
//模拟抛出异常
throw new RuntimeException("取消订单失败");
}
log.info("重试取消第三方订单|SUCC|orderId={}", orderId);
return null;
};
try {
retryTemplate.execute(retry);
} catch (Throwable throwable) {
log.error("重试取消第三方订单|ERROR|orderId={}", orderId, throwable);
}
}



}

单元测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@RunWith(SpringRunner.class)
@SpringBootTest(classes = RetryBootStrap.class)
public class RetryTest {

@Autowired
private RetryService retryService;


@Test
public void testRetryCancel() {
retryService.cancelThirdOrder(10000L);
}


}

执行单元测试方法,可以看到日志:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
2020-07-08 16:15:49.756  INFO 11996 --- [           main] c.l.s.retry.service.RetryService         : 重试取消第三方订单|START|orderId=10000
2020-07-08 16:15:50.058 INFO 11996 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 16:15:50.358 INFO 11996 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 16:15:50.659 INFO 11996 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 16:15:50.960 INFO 11996 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 16:15:50.964 ERROR 11996 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|ERROR|orderId=10000

java.lang.RuntimeException: 取消订单失败
at com.lzumetal.springboot.retry.service.RetryService.lambda$cancelThirdOrder$0(RetryService.java:42) ~[classes/:na]
at org.springframework.retry.support.RetryTemplate.doExecute(RetryTemplate.java:287) ~[spring-retry-1.2.4.RELEASE.jar:na]
at org.springframework.retry.support.RetryTemplate.execute(RetryTemplate.java:164) ~[spring-retry-1.2.4.RELEASE.jar:na]
at com.lzumetal.springboot.retry.service.RetryService.cancelThirdOrder(RetryService.java:48) ~[classes/:na]
at com.lzumetal.springboot.retry.test.RetryTest.testRetryCancel(RetryTest.java:27) [test-classes/:na]
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.8.0_102]
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) ~[na:1.8.0_102]
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.8.0_102]
at java.lang.reflect.Method.invoke(Method.java:498) ~[na:1.8.0_102]
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:50) [junit-4.12.jar:4.12]
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:12) [junit-4.12.jar:4.12]
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:47) [junit-4.12.jar:4.12]
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:17) [junit-4.12.jar:4.12]
at org.springframework.test.context.junit4.statements.RunBeforeTestExecutionCallbacks.evaluate(RunBeforeTestExecutionCallbacks.java:74) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.test.context.junit4.statements.RunAfterTestExecutionCallbacks.evaluate(RunAfterTestExecutionCallbacks.java:84) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.test.context.junit4.statements.RunBeforeTestMethodCallbacks.evaluate(RunBeforeTestMethodCallbacks.java:75) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.test.context.junit4.statements.RunAfterTestMethodCallbacks.evaluate(RunAfterTestMethodCallbacks.java:86) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.test.context.junit4.statements.SpringRepeat.evaluate(SpringRepeat.java:84) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.junit.runners.ParentRunner.runLeaf(ParentRunner.java:325) [junit-4.12.jar:4.12]
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:251) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.runChild(SpringJUnit4ClassRunner.java:97) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:290) [junit-4.12.jar:4.12]
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:71) [junit-4.12.jar:4.12]
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:288) [junit-4.12.jar:4.12]
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:58) [junit-4.12.jar:4.12]
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:268) [junit-4.12.jar:4.12]
at org.springframework.test.context.junit4.statements.RunBeforeTestClassCallbacks.evaluate(RunBeforeTestClassCallbacks.java:61) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.springframework.test.context.junit4.statements.RunAfterTestClassCallbacks.evaluate(RunAfterTestClassCallbacks.java:70) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.junit.runners.ParentRunner.run(ParentRunner.java:363) [junit-4.12.jar:4.12]
at org.springframework.test.context.junit4.SpringJUnit4ClassRunner.run(SpringJUnit4ClassRunner.java:190) [spring-test-5.1.7.RELEASE.jar:5.1.7.RELEASE]
at org.junit.runner.JUnitCore.run(JUnitCore.java:137) [junit-4.12.jar:4.12]
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:68) [junit-rt.jar:na]
at com.intellij.rt.execution.junit.IdeaTestRunner$Repeater.startRunnerWithArgs(IdeaTestRunner.java:47) [junit-rt.jar:na]
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:242) [junit-rt.jar:na]
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:70) [junit-rt.jar:na]

上面的重试机制是重试完之后如果最终没有成功,重试流程自然结束。除了这种方式,也可以指定重试最终没有成功的情况下,去执行一个回退操作。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
/**
* 重试取消订单,并有失败回退策略
*
* @param orderId
*/
public void cancelThirdOrderWithRecoveryCallack(long orderId) {
final RetryCallback<Void, Throwable> retry = context -> {
log.info("重试取消第三方订单|START|orderId={}", orderId);
int i = new Random().nextInt(100);
if (i != 5) {
//模拟抛出异常
throw new RuntimeException("取消订单失败");
}
log.info("重试取消第三方订单|SUCC|orderId={}", orderId);
return null;
};
try {
retryTemplate.execute(retry, context -> {
log.info("重试取消第三方订单失败|执行恢复策略...");
return null;
});
} catch (Throwable throwable) {
log.error("重试取消第三方订单|ERROR|orderId={}", orderId, throwable);
}
}

执行单元测试,如果重试没有成功的情况下会打印如下日志:

1
2
3
4
5
6
2020-07-08 17:10:45.009  INFO 21512 --- [           main] c.l.s.retry.service.RetryService         : 重试取消第三方订单|START|orderId=10000
2020-07-08 17:10:45.311 INFO 21512 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 17:10:45.612 INFO 21512 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 17:10:45.912 INFO 21512 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 17:10:46.213 INFO 21512 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单|START|orderId=10000
2020-07-08 17:10:46.213 INFO 21512 --- [ main] c.l.s.retry.service.RetryService : 重试取消第三方订单失败|执行回退策略...

API说明

RetryOperations是定义重试的接口,它有多个execute方法。

1
2
3
4
5
6
7
8
public interface RetryOperations {

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback) throws E;

<T, E extends Throwable> T execute(RetryCallback<T, E> retryCallback, RecoveryCallback<T> recoveryCallback) throws E;

//后面的省略...
}

RetryCallback定义了需要执行重试的操作,定义好操作后,就是如何重试的问题了。RetryTemplateRetryOperations的模板模式实现,实现了重试和熔断机制。RetryTemplate通过制定不同的重试策略来执行重试逻辑。默认的重试策略是SimpleRetryPlicySimpleRetryPlicy默认会重试3次。如果某次重试成功则后面就不会继续重试了。那么如果3次都重试失败了呢?那么流程结束或者返回兜底结果。要返回兜底结果需要配置RecoveyCallBack,从名字可以看出这是一个兜底回调接口,也就是重试失败后执行的逻辑。

重试策略

上文中的SimpleRetryPolicy是默认的,也是最常使用的策略。除此之外Spring Retry也还提供了多种其他的重试策略,它们都实现了RetryPolicy接口:

  • NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试
  • AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环
  • SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略
  • TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试
  • ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试
  • CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate
  • CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行

回退策略

重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy,上面的示例中我们使用的是FixedBackOffPolicyBackoffRetryPolicy也有多种具体实现:

  • NoBackOffPolicy:无退避算法策略,每次重试时立即重试
  • FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒
  • UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod]之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒
  • ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier
  • ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退

注解开发

除了使用类似上面示例代码的编码方式,Spring Retry还提供了更简单便捷的注解开发模式。

  1. 在启动类上加上@EnableRetry注解。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @SpringBootApplication
    @EnableRetry
    public class RetryBootStrap {

    public static void main(String[] args) {
    new SpringApplicationBuilder(RetryBootStrap.class)
    .registerShutdownHook(true)
    .run(args);
    }

    }
  2. 重试方法上加@Retryable注解,重试回退逻辑的方法上加@Recover注解

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    @Service
    @Slf4j
    public class AnnotationRetryService {


    @Retryable(value = Exception.class, maxAttempts = 5, backoff = @Backoff(delay = 500L))
    public void cancelThirdOrder(long orderId) {
    log.info("重试取消第三方订单|START|orderId={}", orderId);
    int i = new Random().nextInt(100);
    if (i != 5) {
    //模拟抛出异常
    throw new RuntimeException("取消订单失败");
    }
    log.info("重试取消第三方订单|SUCC|orderId={}", orderId);
    }


    @Recover
    public void recover(Exception e) {
    log.info("重试取消第三方订单失败|执行恢复策略...");
    }
    }

使用单元测试方法,同样可以得到和之前同样的效果。

注解说明

  • @EnableRetry:启用重试机制,proxyTargetClass属性为true时(默认false),使用CGLIB代理。
    @EnableRetry注解可以添加在项目启动类上,也可以直接添加在具有@Retryable注解方法的类上。

  • @Retryable:注解需要被重试的方法。

    • include:指定需要处理的异常类。默认为空
    • exclude:指定不需要处理的异常。默认为空
    • value:指定要重试的异常。默认为空
    • maxAttempts:最大重试次数。默认3次
    • backoff:重试等待策略。默认使用@Backoff注解
  • @Backoff:重试回退策略。

    • 不设置参数时,默认使用FixedBackOffPolicy,重试等待1000ms
    • 只设置delay()属性时,使用FixedBackOffPolicy,重试等待指定的毫秒数
    • 当设置delay()和maxDealy()属性时,重试等待在这两个值之间均态分布
    • 使用delay(),maxDealy()和multiplier()属性时,使用ExponentialBackOffPolicy
    • 当设置multiplier()属性不等于0时,同时也设置了random()属性时,使用ExponentialRandomBackOffPolicy
  • @Recover: 用于方法。用于@Retryable失败时的“兜底”处理方法。 @Recover注释的方法必须要与@Retryable注解的方法“签名”保持一致,第一个入参为要重试的异常,其他参数与@Retryable保持一致,返回值也要一样,否则无法执行!

注意

  • FixedBackOffPolicy的回退策略,默认使用的是Thread.sleep方法,会导致当前的线程被阻塞,因此使用的时候要注意。其源码如下:

    1
    2
    3
    4
    5
    6
    7
    8
    protected void doBackOff() throws BackOffInterruptedException {
    try {
    sleeper.sleep(backOffPeriod);
    }
    catch (InterruptedException e) {
    throw new BackOffInterruptedException("Thread interrupted while sleeping", e);
    }
    }
  • @Recover标注的方法必须和@Retryable方法在同一个类中。

------ 本文完 ------