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

本文介绍了在Java开发中处理Http或RPC跨系统调用时的重试策略。通过模拟调用场景,展示了如何使用Java原生代码实现重试逻辑,但指出这种方式存在代码繁琐和耦合度高的问题。然后,文章详细讲解了Guava的retrying工具,展示了如何配置重试条件、等待策略和停止策略,以实现更优雅、解耦的重试逻辑。Guava的retrying工具提供了如固定等待、随机等待、斐波那契递增等多种等待策略,以及灵活的停止策略,提高了代码的可读性和可维护性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

熟悉的重试场景

我们在日常系统开发中,经常会遇到使用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

Guava Retrying 并不是一个官方 Guava 库的一部分,而是一个独立的第三方库,通常被称为 **Retryer** 或者类似的名称。它受到 Google Guava 的启发并扩展了一些功能来支持重试机制。 以下是关于如何找到该库的相关信息: ### 官方网站链接 可以访问以下地址获取更多有关 Guava Retrying 的文档和支持信息: - [GitHub 上的 Guava Retryer](https://github.com/rholder/guava-retrying)[^1] 此项目由 Robert Holden 开发,并提供了灵活的方式来实现方法调用的自动重试逻辑。 ### 使用示例 下面是一段简单的代码展示如何配置和使用 `Retryer` 来执行带重试的操作。 ```java import com.github.rholder.retry.*; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; public class RetryExample { public static void main(String[] args) throws Exception { Retryer<Boolean> retryer = RetryerBuilder.<Boolean>newBuilder() .retryIfResult(result -> !result) .withWaitStrategy(WaitStrategies.fixedWait(1, TimeUnit.SECONDS)) .withStopStrategy(StopStrategies.afterAttempt(3)) .build(); Callable<Boolean> task = () -> performTask(); Boolean result = retryer.call(task); System.out.println("Operation succeeded: " + result); } private static Boolean performTask() { // Simulate a failing operation that eventually succeeds. System.out.println("Attempting task..."); return Math.random() > 0.7; // Random success after some attempts. } } ``` 上述代码展示了如何通过自定义条件设置重试策略以及等待时间间隔[^1]。 ### 注意事项 需要注意的是,在实际生产环境中应用此类工具时,应仔细评估失败场景下的行为模式,以防止不必要的资源消耗或者服务雪崩效应。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值