Springboot实现重试机制
背景
研发工作中时常遇到要和其他服务对接,依赖对方能力的情况,最恶心的是对方提供的服务不稳定,时灵时不灵的,进而影响到自己功能的稳定性。万一发生了这种事,做为研发,咱该怎么办?通过容错直接抛出异常,让用户再试一次?那多low啊!一个优秀的研发很少将问题抛出去,一般都是自己尝试多遍且没办法之后,才会选择将问题反馈给用户。这就要求咱们的相关功能得有重试的能力。今天雷袭实践的课题就是在Springboot项目中实现接口自动重试机制。
代码实践
一、 纯手撸的重试机制
雷袭看到这个课题,第一时间想的是:“这么简单,还需要水个博客?写个try catch加while循环,不是分分钟解决问题吗? 以下是我的第一版代码:
public <T> T retryMethodLocal(Supplier<T> method, int maxRetries) {int retryCount = 0;while (retryCount <= maxRetries) {try {// 尝试执行方法return method.get();} catch (Exception e) {retryCount++;if (retryCount > maxRetries) {// 超过最大重试次数后抛出异常throw e;}System.out.println("第 " + retryCount + " 次重试...");}}return null; // 理论上不会到达这里}//测试方法,用于模拟一个不稳定的接口。当随机小数小于0.8时,抛错,public String testMethod(){StringBuilder builder = new StringBuilder("进入到testMethod方法");if (date != null){builder.append(",与上一次的时间间隔为:" + ( new Date().getTime()-date.getTime()));}date = new Date();log.info(builder.toString());// 示例:随机失败double random = Math.random();if (random < 0.8) {log.error("调用失败 " + random);throw new RuntimeException("调用失败 " + random);}log.error("调用成功 " + random);return "成功";}
Controller方法如下:
@GetMapping("/retryOne")public Object retryOne() {return leixiRetryService.retryMethodLocal(leixiRetryService::testMethod, 5);}
通过浏览器访问链接:http://127.0.0.1:19200/leixi/retryOne , 以下是控制台信息:
根据控制台的日志可知,重试机制是生效的。
二、通过guava-retrying 实现重试
以上方法虽然能解决问题,但是它太原生了。像重试这样的基础机制,Springboot肯定提供了相关的能力或组件,咱们完全不用自己写。在咨询了同事后,我又剽来了一种常用的方法,实现如下:
pom.xml增加依赖:
<dependency><groupId>com.github.rholder</groupId><artifactId>guava-retrying</artifactId><version>2.0.0</version></dependency>
service层添加方法:
public <T> T retryMethodByRetryer(Supplier<T> method, Integer maxRetries, Integer waitTime) {Retryer<T> retryer = RetryerBuilder.<T>newBuilder()//.retryIfResult(result -> !result.equals("成功")) //还可以通过返回结果判定是否重试.retryIfException().withWaitStrategy(WaitStrategies.fixedWait(waitTime, TimeUnit.MILLISECONDS)).withStopStrategy(StopStrategies.stopAfterAttempt(maxRetries)).withRetryListener(new RetryListener() {@Overridepublic <V> void onRetry(Attempt<V> attempt) {log.info("第【{}】次重试,距离首次调用已过【{}】ms", attempt.getAttemptNumber(),attempt.getDelaySinceFirstAttempt());}}).build();try {return retryer.call(method::get);} catch (RetryException | ExecutionException e) {log.error(">>> 重试多次,远程获取批量报告文档url失败 <<<", e);}return null ;}
Controller层增加方法:
@GetMapping("/retryTwo")public Object retryTwo() {return leixiRetryService.retryMethodByRetryer(() -> leixiRetryService.testMethod(), 5, 100);}
通过浏览器访问链接:http://127.0.0.1:19200/leixi/retryTwo , 以下是控制台信息:
该方法相比于第一版就优雅了很多,它通过封装的重试对象,可以精准的捕获到调用的次数,间隔时间,调用结果,还可以手动设置调用间隔,对于触发调用的条件也更加灵活。
三、 通过spring-retry实现重试
除了上述方法,Spring 还提供了一种重试策略,通过注解来实现,代码如下:
添加pom依赖:
<!-- 重试 --><dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><dependency><groupId>org.aspectj</groupId><artifactId>aspectjweaver</artifactId><version>1.9.6</version></dependency>
Application.java中添加重试注解:@EnableRetry
Service层添加方法:
@Retryable(maxAttempts = 5, backoff = @Backoff(value = 100L, multiplier = 1.5))public String retryByTarget() {return testMethod();}
Controller中增加方法:
@GetMapping("/retryThree")public Object retryThree() {return leixiRetryService.retryByTarget();}
通过浏览器访问链接:http://127.0.0.1:19200/leixi/retryThree , 以下是控制台信息:
相比于前两种方式,这种方法更加优雅,代码量更少,且通过注解的方式,可以很容易的对功能进行大批量的修改。请各位留意@Retryable中的@Backoff注解,它控制接口调用的间隔时间梯度递增,如第一次间隔100ms,第二次间隔100*1.5ms,第三次间隔100*1.5*1.5,以此类推,如此很好的避免了有故障的服务在短时间内接到大量重试调用,从而导致问题恶化的情况。
小记
要注意的是,配置了重试机制的功能,需要保证它在事务上是幂等的。即无论第一次执行成功与否,再执行第二次时,其结果都不会改变。多次执行的结果是一致的。
雷袭很喜欢在工作中捕捉到能令自己惊艳的东西,比较各种工具/方法的不同实现方式,以增长见闻,提升眼界。每每发现到自己又有新的发现,总是欣喜莫名,程序员的乐趣,不外如是。