如何在业务逻辑中引入重试机制:spring-Retry和Guava-Retry

发布于:2023-02-02 ⋅ 阅读:(565) ⋅ 点赞:(0)

前言

为什么要引入重试机制?

        我们首先看看正常的业务系统交互流程,就像下面图中所示一样,我们自己开发的系统通过HTTP接口或者通过RPC去访问其他业务系统,其他系统在没出现任何问题的情况下会返回给我们需要的数据,状态为success。

        但大家在日常的开发工作当中应该碰到过不少这样的问题:自己应用因为业务需求需要调其他关联应用的接口或二方包,而其他应用的接口稳定性不敢过分恭维,老是出一些莫名奇妙的幺蛾子,比如,由于接口暂时升级维护导致的短暂不可用,又或者网络抖动因素导致的单次接口请求失败。

        诸如此类的麻烦问题会因为业务强依赖致使我们自己维护的系统也跟着陷入一种不稳定的状态(当然这个强依赖是没有办法的事情,毕竟业务之间需要解耦独立开发维护)。

        所以,也就是说重试的使用场景大多是因为我们的系统依赖了其他的业务,或者是由于我们自己的业务需要通过网络请求去获取数据这样的场景。既然一次请求结果的状态非常不可控、不稳定,那么一个非常自然的想法就是多试几次,就能很好的避开网络抖动或其他关联应用暂时down机维护带来的系统不可用问题。


答疑解惑

当然,这里也有几个引入重试机制以后需要考虑的问题需要我们先解决一下:

  1. 我们应该重试几次?
  2. 每次重试的间隔设置为多少合适?
  3. 如果所有重试机会都用完了还是不成功怎么办?

问题1:重试几次合适?

        通常来说我们单次重试所面临的情况就如上面我们分析的一样,有很大的不可确定性,那到底多少次是比较合理的次数呢?这个就要“具体业务具体分析”了,但一般来说3次重试就差不多可以满足大多数业务需求了,当然,这是需要结合后面要说的重试间隔一起讨论的。

        为什么说3次就基本够了呢,因为如果被请求系统实在处于长时间不可用状态。我们重试多次是没有什么意义的。重试的同时还要注意请求时间的问题,如果存在超时风险,那么对第三方的请求方法就一概是异步进行的。

问题2:重试间隔设置为多少合适?

        如果重试间隔设置得太小,可能被调用系统还没来得及恢复过来我们就又发起调用,得到的结果肯定还是Fail;

        如果设置的太大,我们自己的系统就会牺牲掉不少数据时效性。所以,重试间隔也要根据被调用的系统平均恢复时间去正确估量,通常而言这个平均恢复时间很难统计到,所以一般的经验值是3至5分钟。

问题3:重试机会用完以后依旧Fail怎么办?

        这种情况也是需要认真考虑的,因为不排除被调用系统真的起不来的情况,这时候就需要采取一定的补偿措施了。

  • 首先,要做的就是在我们自己的系统增加错误报警机制,这样我们才能即时感知到应用发生了不可自恢复的调用异常。
  • 其次,就是在我们的代码逻辑中加入触发手动重试的开关,这样在发生异常情况以后我们就可以方便的修改触发开关然后手动重试。

        在这里还有一个非常重要的问题需要考虑,那就是接口调用的幂等性问题,如果接口不是幂等的,那我们手动重试的时候就很容易发生数据错乱相关的问题。


Spring-Retry 和 Guava-Retry

        Spring-Retry 和 Guava-Retry 工具都可以再项目中为我们引入重试机制,而且都是线程安全的重试,先简具体介绍下使用方法:

1. Spring-Retry 用法

        Spring 为我们提供了原生的重试类库,我们可以方便地引入到工程当中,利用它提供的重试注解,没有太多的业务逻辑侵入性。

        但是,Spring 的重试机制也存在一定的不足,只支持对异常进行捕获,而无法对返回值进行校验。在实际开发中,必须将重试的场景封装成自定义异常以后才可以捕获,而这种靠异常来控制业务流程的做法是开发大忌,导致spring-Retry 的适用场景就收到了很大限制。

        所以,这种方法我只简单分享一下,更详细的用法和属性大家参阅《Spring Retry的文档》就好了,解释的非常清楚。推荐一片文章:《重试框架Guava-Retry和spring-Retry用法介绍》

  • 首先,pom.xml引入相关依赖包;
<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.2.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.aspectj</groupId>
    <artifactId>aspectjweaver</artifactId>
    <version>1.9.1</version>
</dependency>
  •  然后,在启动类或者配置类上添加@EnableRetry注解;

@SpringBootApplication
@EnableRetry
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}
  • 最后,在需要重试的方法上添加@Retryable注解或@Recover注解。这两个注解功能不同,下面由具体说明;
// 需要重试所调用的方法
@Retryable
public String call(){
    long times = helloTimes.incrementAndGet();
    log.info("hello times:{}", times);
    if (times % 4 != 0){
        log.warn("发生异常,time:{}", LocalTime.now() );
        throw new HelloRetryException("发生Hello异常");
    }
    return "hello";
}

// 达到最大重试次数,或抛出了一个没有指定进行重试的异常所调用的方法
@Recover
public String recover(){
    log.info("重试失败的业务逻辑。");
    return "fail";
}

2. Guava-Retry 用法

        相比 Spring Retry,Guava Retry具有更强的灵活性,不仅支持多个异常的重试源定义,还可以根据返回值校验来判断是否需要进行重试。

        用的时候也很简单,先创建一个Retryer实例,然后使用这个实例对需要重试的方法进行调用,可以通过很多方法来设置重试机制,比如:

  1. 使用 retryIfException 来对所有异常进行重试;
  2. 使用 retryIfExceptionOfType 方法来设置对指定异常进行重试;
  3. 使用 retryIfResult 来对不符合预期的返回结果进行重试;
  4. 使用 retryIfRuntimeException 方法来对所有 RuntimeException 进行重试。
  • 首先,pom.xml引入相关依赖包;
<dependency>
    <groupId>com.github.rholder</groupId>
    <artifactId>guava-retrying</artifactId>
    <version>2.0.0</version>
</dependency>
  • 定义 Retryer 实例

@Slf4j
@Service
public class RetryDemoTask {

    public static Retryer<Boolean> retryer;

    // RetryerBuilder 构建重试实例 retryer,
    // 可以设置重试源且可以支持多个重试源,可以配置重试次数或重试超时时间,以及可以配置等待时间间隔
    static {
        retryer = RetryerBuilder.<Boolean> newBuilder()
                .retryIfExceptionOfType(RemoteAccessException.class)    // 设置异常重试源                    
                .retryIfResult(res-> res==false)    // 设置根据结果重试                                      
                .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS)) // 设置等待间隔时间
                .withStopStrategy(StopStrategies.stopAfterAttempt(3)) // 设置最大重试次数
                .build();
    }
    // 测试的重试方法
    public static boolean randomTask(String param)  {
        int i = RandomUtils.nextInt(0,10);
        if (i < 3) {
            log.info("随机生成的数:{},小于3,模拟校验失败,不需要重试。", i);
            throw new IllegalArgumentException("参数异常");
        } else if (i  < 5) {
            log.info("随机生成的数:{},小于5,模拟网络抖动,需要重试。", i);
            throw new RemoteAccessException("自定义异常");
        } else if (i  < 7) {
            log.info("随机生成的数:{},小于7,模拟请求失败,返回 false", i);
            return false;
        }
        log.info("随机生成的数:{},大于7,模拟请求成功,返回 true", i);
        return true;
    }
}
  • Junit 测试一下

@Slf4j
public class RetrryTest {
    @Resource
    private SpringRetryDemo springRetryDemo;

    @Test
    void spring_retry_test() {
        log.info("---------------- 测试开始 ---------------");
        try {
            RetryDemoTask.retryer.call(() -> RetryDemoTask.randomTask("测试 gunva-retry 重试机制"));
        } catch (IllegalArgumentException e) {
            log.error(e.getMessage());
        } catch (Exception e) {
            log.error(e.getMessage());
        }
        log.info("---------------- 测试结束 ---------------");
    }
}
  • 测试结果分析:

        1. 返回成功,不需要重试;

        2. 返回失败或者定义重试的异常,需要重试;

        3. 遇到未定义的异常,直接返回,不需要重试;

        4. 超过设定的重试次数,不再重试;


总结

        Spring-Retry 和 Guava-Retry 工具都是线程安全的重试,能够支持并发业务场景的重试逻辑正确性。两者都很好的将正常方法和重试方法进行了解耦,可以设置超时时间,重试次数,间隔时间,监听结果,都是不错的框架,但是,明显感觉得到 Guava-Retry 在使用上更便捷,更灵活。

        相比Spring,Guava Retry提供了几个核心特性:

  • 可以设置任务单次执行的时间限制,如果超时则抛出异常。
  • 可以设置重试监听器,用来执行额外的处理工作。
  • 可以设置任务阻塞策略,即可以设置当前重试完成,下次重试开始前的这段时间做什么事情。
  • 可以通过停止重试策略和等待策略结合使用来设置更加灵活的策略,比如指数等待时长并最多10次调用,随机等待时长并永不停止等等。

        上面针对我们引入重试机制需要思考的几个核心问题,以及为重试机制提供良好支持的工具类库都分别作了简单介绍,相信大家在今后的开发工作中遇到类似场景也能驾轻就熟地使用思考了。

        我们日常工作中有很多“大”的业务场景需要我们集中精力去突破、去思考,但也有很多类似的“小”点需要我们去打穿、吃透,大家共勉。


本文含有隐藏内容,请 开通VIP 后查看

网站公告

今日签到

点亮在社区的每一天
去签到