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

来源:互联网 发布:excel单元格数据分行 编辑:程序博客网 时间:2024/06/10 13:52

假设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

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<String, Throwable> retryCallback = context -> remoteInvoke();
RecoveryCallback<String> 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))

其他主题

监听器和监控


监听器接口如下:

// 第一次重试的时候会执行该方法
<T, E extends Throwable> boolean open(RetryContext context, RetryCallback<T, E> callback);
// 重试结束后会调用改方法
<T, E extends Throwable> void close(RetryContext context, RetryCallback<T, E> callback, Throwable throwable);
// 每次重试产生异常时会调用改方法
<T, E extends Throwable> void onError(RetryContext context, RetryCallback<T, E> 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<String, Throwable>)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.<Class<? extends Throwable>, Boolean>singletonMap(Exception.class, true));        // 设置重试回退操作策略,主要设置重试间隔时间        FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();        fixedBackOffPolicy.setBackOffPeriod(100);        retryTemplate.setRetryPolicy(policy);        retryTemplate.setBackOffPolicy(fixedBackOffPolicy);// 通过RetryCallback 重试回调实例包装正常逻辑逻辑,第一次执行和重试执行执行的都是这段逻辑        final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, 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<Object> recoveryCallback = new RecoveryCallback<Object>() {            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
阅读全文
'); })();
0 0
原创粉丝点击
热门IT博客
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 认输赵智郑娇 让苍天知道我不认输 让苍天知道我不认输表情包 他在岁月面前认了输 不许掉出来他笑着按下 宇智波称霸 不许笑ha 明星需不需要人设 不设房 不识枕边人 白蔹 天下无人不识君 天下谁人不识君的上一句 目不识丁的人叫什么 已是曲中人不识曲终意 天下谁人不识君的君是谁 不识曲中意已是曲中人 射雕之男儿行 人不识 识相 不识相 都市特种战兵 不言不语 不语兵 不想说话图片 不要和陌生人说话全集 不要陌生人说话全集 站着说话不腰疼 你怎么不说话 怎么不说话 不说话 为什么不说话 我不说话 不想说话 不说话图片 不说话的成语 不说话了 我不会说话 别跟我说话 别说话了 不要和我说话 别说话 不愿意说话 我不说话不代表我不知道 为什么不会说话