使用Guava-retrying优雅地解决异常重试场景

x33g5p2x  于2021-09-19 转载在 其他  
字(3.5k)|赞(0)|评价(0)|浏览(262)

熟悉的重试场景

我们在日常系统开发中,经常会遇到使用Http或者RPC调用跨系统应用的场景。由于是跨系统间调用,不可避免地会遇到网络问题或者服务方限流等原因导致的异常,这时我们就需要对失败的调用进行重试,这会引入了一系列的问题:

哪些异常需要重试?

应该重试多少次?

重试的时间间隔是多少?

每次重试时间的累加如何设定?

超时时间是多少?

场景模拟

我们使用代码来模拟实际场景,MockService模拟一个远程调用,使用Random随机数模拟返回的请求结果。为了让重试行为更加明显,这里设置了返回随机数[0, 9],只有返回0才是请求成功,剩余情况都是超时,也就是十分之一的调用成功率。

@Slf4j
public class MockService {
		
  	// 模拟远程服务调用 
    public static Response call() {
        Random rand = new Random();
        int result = rand.nextInt(10);

        if(result == 0) {       // 成功
            return new Response(200, "处理成功");
        } else {
            try {
                Thread.sleep(1 * 1000);
            } catch (Exception e) {
                log.error(e.getMessage(), e);
            }
            throw new RuntimeException("处理超过1s,超时");
        }
    }
}
@AllArgsConstructor
@Data
public class Response {

    private int code;       // 状态码

    private String msg;     // 返回结果
}

Java原生的解决方案

public Response commonRetry() {
        int retryTimes = 1;
        while(retryTimes <= 10) {
            try {
                log.info("第{}次调用", retryTimes);
                Response response = MockService.call();
                if(response.getCode() == 200) {
                    return response;
                }
                Thread.sleep(1000);		// 失败,等待下次调用
                retryTimes++;
            } catch (Exception e) {
                log.error(e.getMessage(), e);
                try {
                    Thread.sleep(1000);
                } catch (Exception e2) {
                    log.error(e2.getMessage(), e2);
                }
                retryTimes++;
            }
        }
        throw new RuntimeException("重试失败");
    }

一般我们可以通过try/catch的方式来设置重试的行为,retryTimes代表重试的次数,每次调用时查看结果的返回码,如果是200则返回结果,如果不是200或者服务抛异常则等待1秒钟,然后重试直到达到最大的重试次数。

可以看到代码比较繁琐,可读性较差。另外,重试策略和业务处理的代码耦合,如果再考虑不同异常的处理方式和重试间隔时间的累加,代码会更加复杂。

Guava的retrying工具

Guava提供了专门的重试工具来帮我们进行解耦,它对重试逻辑进行了抽象,提供了多种重试策略,而且扩展起来非常方便,可以监控每次充实的结果和行为,提升远程调用方代码的简洁性与实用性。

使用前,我们需要引入guava-retrying包

<dependency>
            <groupId>com.github.rholder</groupId>
            <artifactId>guava-retrying</artifactId>
            <version>2.0.0</version>
        </dependency>

使用方式相当简单,首先声明Retryer对象,通过链式调用配置重试策略,再通过回调函数实现代码逻辑,

public Response graceRetry() {
        Retryer<Response> retryer = RetryerBuilder.<Response>newBuilder()
                .retryIfException()		// 当发生异常时重试
                .retryIfResult(response -> response.getCode() != 200)		// 当返回码不为200时重试
                .withWaitStrategy(WaitStrategies.fibonacciWait(1000, 10, TimeUnit.SECONDS))	// 等待策略:使用斐波拉契数列递增等待
                .withStopStrategy(StopStrategies.stopAfterAttempt(10))		// 重试达到10次时退出
                .build();
        try {
            return retryer.call(new Callable<Response>() {
                @Override
                public Response call() throws Exception {
                    log.info("重试调用");
                    return MockService.call();
                }
            });
        } catch (Exception e) {
            log.error(e.getMessage(), e);
        }
        throw new RuntimeException("重试失败");
    }

日志结果如下:

19:20:01.862 [main] INFO RetryTest - 重试调用
19:20:03.868 [main] INFO RetryTest - 重试调用
19:20:05.874 [main] INFO RetryTest - 重试调用
19:20:08.882 [main] INFO RetryTest - 重试调用
19:20:12.892 [main] INFO RetryTest - 重试调用
19:20:18.897 [main] INFO RetryTest - 重试调用
19:20:27.900 [main] INFO RetryTest - 重试调用
19:20:38.903 [main] INFO RetryTest - 重试调用
19:20:49.909 [main] INFO RetryTest - 重试调用
19:21:00.919 [main] INFO RetryTest - 重试调用
19:21:00.921 [main] INFO RetryTest - Response(code=200, msg=处理成功)

可以看出,每次重试的时长根据斐波拉契数列递增,直到递增到每次10秒后就不再递增,并且实际调用在10次内成功。

重试的实现逻辑全部通过配置策略来实现,实现了代码的解耦,可读性显著提升。

下面是主要用到的重试与停止策略:

  • retryIfException() 表示抛出Exception异常及其子异常时重试

  • retryIfResult(response) 可通过返回的response对象判断哪些返回码需要重试

  • withWaitStrategy(WaitStrategies) 配置等待策略,常见的有:

  • WaitStrategies.fixedWait() 固定等待n的时间

  • WaitStrategies.randomWait() 等待随机时间后重试,可配置等待上限和等待下限

  • WaitStragegies.incrementingWait() 按照等差数列增加等待时间

  • WaitStragegies.exponentialWait() 按照指数级别增长等待时间

  • WaitStragegies.fibonacciWait() 按照斐波拉契数列增加等待时间

  • withStopStrategy(StopStrategies) 配置停止重试的策略,常见的有:

  • StopStrategies.neverStop() 一直重试,直到返回成功为止

  • StopStrategies.stopAfterAttempt() 重试多少次停止

  • StopStrategies.stopAfterDelay() 一直重试直到成功或者超过设置的时长为止

  • withRetryListener() 注册一个回调函数,当重试时记录重试失败的次数或者日志

小小收获

可以看出,和自己实现的代码相比,使用Guava的retrying小工具实现的重试代码简洁很多,重用性相当高,代码API也设计得相当优雅。

我们在日常的代码编写中,也应该多学习开源项目的设计思路和实现,体会高手是如何对代码进行抽象与解耦,对自己的代码水平也是很好的提高与锻炼。

written by Ryan.Ou

相关文章