利用Spring-Retry定制化你的RPC重试

假设A,B两个系统,当A->B的调用失败时,我们可以采取何种策略?以下是常见的策略:

  1. failfast,即快速失败像上层抛出远程调用异常
  2. failover,即A->B失败,选择集群其他机器A->Bn(1…N)
  3. failsafe,失败吞异常
  4. failback, 过一会再重试,比如网络抖动,等一小会在重试。

其中failback则是本文所要讨论的核心内容。有些rpc框架提供failback策略,比如dubbo,但dubbo的failback仅仅只是设置重试次数,功能单一。更多的RPC框架则懒得管这事儿,他们将failback策略实现交给用户自己去做,你爱怎么做就怎么做。

那么思考一下,如果我们要自己做failback,比如如下场景:XXXFacade调用失败后,我们等20毫秒在重试,一共重试5次,如果依旧失败则打一条日志出来。一般人的实现,会做一个for(int n; n < 5;)循环,并且catch XXXFacade异常后sleep(5s),最后判断n的次数。这个办法确实能实现该功能,但是重复早轮子,并且功能扩展性也不高。本文教大家使用spring-retry框架实现可定制化的调用重试策略。文章主要包含以下几个方面:

  • 整体概览
  • retry策略
  • backoff策略
  • 其他方面
  • 实现文章开头的例子

框架概览


Spring Retry 框架广泛使用于Spring Batch,Spring Integration,Spring for Apache Hadoop等spring项目,由于spring-try源代码简单明确,本文不会讲其实现,只讲解其大体执行流和相应的框架抽象。其整体思路大致如下图所示:

利用Spring-Retry定制化你的RPC重试_第1张图片

1. RetryTemplate,重试模板,是进入spring-retry框架的整体流程入口

  1. RetryCallback,重试回调,用户包装业务流,第一次执行和产生重试执行都会调用这个callback代码
  2. RetryPolicy,重试策略,不同策略有不同的重试方式
  3. BackOffPolicy,两次重试之间的回避策略,一般采用超时时间机制
  4. RecoveryCallback,当所有重试都失败后,回调该接口,提供给业务重试回复机制
  5. RetryContext,每次重试都会将其作为参数传入RetryCallback中使用
  6. RetryListener,监听重试行为,主要用于监控。

当RetryCallback的调用产生异常的时候,框架首先会通过我们设置的RetryPolicy判断本次异常是否需要重试,如果需要重试,则调用BackOffPolicy,回退一定时间后,在重新调用RetryCallback。如果所有重试都失败了,则退出重试,调用RecoveryCallback退出框架。

使用示例如下:

 
       
public static void main(String[] args) throws Throwable {
// 新建一个模板,可用作为我一个spring bean注入进来
RetryTemplate template = new RetryTemplate();
RetryCallback retryCallback = context -> remoteInvoke();
RecoveryCallback recoveryCallback = context -> {System.out.println( "recovery"); return "recovery";};
// 设置回避策略
template.setBackOffPolicy( new FixedBackOffPolicy());
// 设置策略
template.setRetryPolicy( new SimpleRetryPolicy( 5));
// 设置listener
template.setListeners( new RetryListener[]{ new StatisticsListener( new DefaultStatisticsRepository())});
// 执行模板
template.execute(retryCallback, recoveryCallback);
}

重试策略

支持的重试策略如下:

  1. NeverRetryPolicy:只调用RetryCallback一次,不重试;
  2. AlwaysRetryPolicy:无限重试,最好不要用
  3. SimpleRetryPolicy:重试n次,默认3,也是模板默认的策略。很常用
  4. TimeoutRetryPolicy:在n毫秒内不断进行重试,超过这个时间后停止重试
  5. CircuitBreakerRetryPolicy:熔断功能的重试,关于熔断,请参考:使用hystrix保护你的应用
  6. ExceptionClassifierRetryPolicy: 可以根据不同的异常,执行不同的重试策略,很常用
  7. CompositeRetryPolicy:将不同的策略组合起来,有悲观组合和乐观组合。悲观默认重试,有不重试的策略则不重试。乐观默认不重试,有需要重试的策略则重试。

下面简单思考一下以上策略的实现方式。

  1. NeverRetryPolicy:判断是否重试的时候,直接返回false
  2. AlwaysRetryPolicy:判断是否重试的时候,直接返回true
  3. SimpleRetryPolicy:通过一个计数n,每次重试自增
  4. TimeoutRetryPolicy:保存第一次重试时间,每次进行重试判断 当前毫秒时间-第一次重试时间 > 设置的时间间隔
  5. CircuitBreakerRetryPolicy:与4类似
  6. ExceptionClassifierRetryPolicy:采用一个Map实现,每次异常的时候,拿到对应重试策略,在重试即可
  7. CompositeRetryPolicy:使用数据依次保存策略,执行的时候,顺序执行即可

回避策略

支持的回避策略如下:

  1. NoBackOffPolicy:不回避
  2. FixedBackOffPolicy:n毫秒退避后再进行重试
  3. UniformRandomBackOffPolicy:随机选择一个[n,m](如20ms,40ms)回避时间回避后,然后在重试
  4. ExponentialBackOffPolicy:指数退避策略,休眠时间指数递增
  5. ExponentialRandomBackOffPolicy:随机指数退避,指数中乘积会混入随机值

以上有两点需要注意:

  1. 如何执行回避?一般使用ThreadWaitSleeper,即当前线程直接sleep一段时间。
  2. 凡是带有随机性的策略,大多都是为了避免惊群效应,防止相同时间执行大量操作。

下面思考一下以上策略的实现方式:

  1. NoBackOffPolicy:直接返回即可
  2. FixedBackOffPolicy`: 直接通过Sleeper设置n秒即可
  3. UniformRandomBackOffPolicy: FixedBackOffPolicy + Random()
  4. ExponentialBackOffPolicy:T = initial; T = T + T * multiplier
  5. ExponentialRandomBackOffPolicy:T = initial; T = (T + T multiplier) (1 + randomFloat() * (multiplier - 1))

其他主题

监听器和监控


监听器接口如下:

 
       
// 第一次重试的时候会执行该方法
boolean open(RetryContext context, RetryCallback callback);
// 重试结束后会调用改方法
void close(RetryContext context, RetryCallback callback, Throwable throwable);
// 每次重试产生异常时会调用改方法
void onError(RetryContext context, RetryCallback callback, Throwable throwable);

我理解的监听器是一个很鸡肋的功能,主要用于监控模板执行情况:new StatisticsListener(new DefaultStatisticsRepository())

有状态和无状态重试

上文讨论的都是无状态重试,这意味着产生异常,并不会将其抛出去,对于事务性调用,这是不可容忍的,因为上层框架需要获得异常进行事务的回滚操作。此时应当使用有状态重试。如下代码示例:

 
       
BinaryExceptionClassifier classifier = new BinaryExceptionClassifier(
Collections.singleton(Throwable.class)
);
RetryState state = new DefaultRetryState( "mykey", false, classifier);
// 有状态
String result = template.execute((RetryCallback)context -> {
remoteInvoke();
return "ret";
}, context -> {
return "recovery";
}, state);

事务的开销一般较大,这里虽然能够有状态重试进行事务的回滚,但并不建议去进行事务的重试,而应当使用failfast机制,可能更加合理一些。

实现文章开头的例子

 
       
public class Test1 {

    public static Boolean vpmsRetryCoupon(final String userId) {
        // 构建重试模板实例
        RetryTemplate retryTemplate = new RetryTemplate();
        // 设置重试策略,主要设置重试次数
        SimpleRetryPolicy policy = new SimpleRetryPolicy(10, Collections.extends Throwable>, Boolean>singletonMap(Exception.class, true));
        // 设置重试回退操作策略,主要设置重试间隔时间
        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();
        fixedBackOffPolicy.setBackOffPeriod(100);
        retryTemplate.setRetryPolicy(policy);
        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);
// 通过RetryCallback 重试回调实例包装正常逻辑逻辑,第一次执行和重试执行执行的都是这段逻辑
        final RetryCallback, Exception> retryCallback = new RetryCallback, Exception>() {
            //RetryContext 重试操作上下文约定,统一spring-try包装
            public Object doWithRetry(RetryContext context) throws Exception {
                boolean result = pushCouponByVpmsaa(userId);
                if (!result) {
                    throw new RuntimeException();//这个点特别注意,重试的根源通过Exception返回
                }
                return true;
            }
        };
// 通过RecoveryCallback 重试流程正常结束或者达到重试上限后的退出恢复操作实例
        final RecoveryCallback recoveryCallback = new RecoveryCallback() {
            public Object recover(RetryContext context) throws Exception {
//                logger.info("正在重试发券::::::::::::"+userId);
                return null;
            }
        };
        try {
            // 由retryTemplate 执行execute方法开始逻辑执行
            retryTemplate.execute(retryCallback, recoveryCallback);
        } catch (Exception e) {
//            logger.info("发券错误异常========"+e.getMessage());
            e.printStackTrace();
        }
        return true;
    }

    public static void main(String[] args) {
        vpmsRetryCoupon("43333");
    }


    public static Boolean pushCouponByVpmsaa(String userId) {
        Random random = new Random();
        int a = random.nextInt(10);
        System.out.println("a是" + a);
        if (a == 8) {
            return true;
        } else {
            return false;
        }
    }
} 
       

注:如果这个例子采用spring xml配置方式将会更加简洁,策略的创建你都省了

参考资料

  • srping-try github

你可能感兴趣的:(spring)