• 异常重试框架Spring Retry实践


    前期准备
    在Maven项目中添加Spring Retry和切面的依赖

    POM:

            <!-- Spring Retry -->
    
            <dependency>
    
                <groupId>org.springframework.retry</groupId>
    
                <artifactId>spring-retry</artifactId>
    
            </dependency>
    
            <dependency>
    
                <groupId>org.aspectj</groupId>
    
                <artifactId>aspectjweaver</artifactId>
    
            </dependency>


    注解方式实践
    Service类:@Retryable创建了一个最大重试次数为3,最初重试等待时间为5秒,重试等待时间倍率为1的接口;@Recover当@Retryable接口重试3次后依然不成功则会运行。

    package com.jsoft.springboottest.springboottest1;
    import java.time.LocalTime;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.remoting.RemoteAccessException;
    import org.springframework.retry.annotation.Backoff;
    import org.springframework.retry.annotation.Recover;
    import org.springframework.retry.annotation.Retryable;
    import org.springframework.stereotype.Service;
    
    @Service
    public class RemoteService {
        
        private final static Logger logger = LoggerFactory.getLogger(RemoteService.class);
        
        @Retryable(value = { RemoteAccessException.class }, maxAttempts = 3, backoff = @Backoff(delay = 5000l, multiplier = 1))
        public void call() throws Exception {
            logger.info(LocalTime.now()+" do something...");
            throw new RemoteAccessException("RPC调用异常");
        }
    
        @Recover
        public void recover(RemoteAccessException e) {
            logger.info(e.getMessage());
        }
    }


    Controller:创建一个调用重试接口的接口

    package com.jsoft.springboottest.springboottest1.controller;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.jsoft.springboottest.springboottest1.RemoteService;
    
    @RestController
    public class TestController {
        
        @Autowired
        private RemoteService remoteService;
        
        @RequestMapping("/show")
        public String show(){
            try {
                remoteService.call();
            } catch (Exception e) {
                e.printStackTrace();
            }
            return "Hello World";        
        }
    }


    运行结果如下:

    2018-11-07 17:01:55.463  INFO 8136 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : 17:01:55.463 do something...
    2018-11-07 17:02:00.463  INFO 8136 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : 17:02:00.463 do something...
    2018-11-07 17:02:05.463  INFO 8136 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : 17:02:05.463 do something...
    2018-11-07 17:02:05.464  INFO 8136 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : RPC调用异常
    注意:

    1、使用了@Retryable的方法里面不能使用try...catch包裹,要在方法上抛出异常,不然不会触发。

    2、在重试期间这个方法是同步的,如果使用类似Spring Cloud这种框架的熔断机制时,可以结合重试机制来重试后返回结果。

    3、@Recover 用于@Retryable重试失败后处理方法,此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。

    注解简介
    3.1  @Retryable注解

    被注解的方法发生异常时会重试 

    value:指定发生的异常进行重试 
    include:和value一样,默认空,当exclude也为空时,所有异常都重试 
    exclude:指定异常不重试,默认空,当include也为空时,所有异常都重试 
    maxAttemps:重试次数,默认3 
    backoff:重试补偿机制,默认没有
    3.2  @Backoff注解

    delay:指定延迟后重试 
    multiplier:指定延迟的倍数,默认为1。比如delay=5000l,multiplier=2时,第一次重试为5秒后,第二次为5*2=10秒,第三次为5*2*2=20秒
    3.3  @Recover 

    当重试到达指定次数时,被注解的方法将被回调,可以在该方法中进行日志处理。需要注意的是发生的异常和入参类型一致时才会回调。

    3.4 @EnableRetry

    开启重试。

    非注解方式实践
    Spring Retry不只能注入方式去实现,还可以通过API的方式实现,类似熔断处理的机制就基于API方式实现会比较宽松。

    代码如上面所示,只需要修改Service中的API。

    public void test() {
    
        final RetryTemplate retryTemplate = new RetryTemplate();
    
        final SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean>  singletonMap(Exception.class, true));
    
        ExponentialBackOffPolicy ePolicy = new ExponentialBackOffPolicy();
    
        ePolicy.setMultiplier(2);//等待时间倍率
    
        ePolicy.setInitialInterval(2000l);//重试间隔时间,2秒
    
        ePolicy.setMaxInterval(30 * 60 * 1000l);//最大重试间隔时间为30分钟
    
        retryTemplate.setRetryPolicy(policy);
    
        retryTemplate.setBackOffPolicy(ePolicy);
    
        final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() {
    
            public Object doWithRetry(RetryContext context) throws Exception {
    
                logger.info(LocalTime.now() + "do some thing,第" + context.getRetryCount() + "次重试");
                    //设置context一些属性,给RecoveryCallback传递一些属性
                    context.setAttribute("key1", "value1");
                    throw new Exception("RPC调用异常");        }
    
        };
    
    
    
        // 如果RetryCallback执行出现指定异常, 并且超过最大重试次数依旧出现指定异常的话,就执行RecoveryCallback动作
    
        final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {
    
            public Object recover(RetryContext context) throws Exception {
    
                logger.info(context.getLastThrowable().getMessage());
                return null;
    
            }
    
        };
    
    
    
        try {
    
            final Object execute = retryTemplate.execute(retryCallback, recoveryCallback);
    
        } catch (Exception e) {
    
            e.printStackTrace();
    
        }
    
    }
    public void test() {
    
        final RetryTemplate retryTemplate = new RetryTemplate();
    
        final SimpleRetryPolicy policy = new SimpleRetryPolicy(3, Collections.<Class<? extends Throwable>, Boolean>
    
                singletonMap(Exception.class, true));
    
        ExponentialBackOffPolicy ePolicy = new ExponentialBackOffPolicy();
    
        ePolicy.setMultiplier(2);//等待时间倍率
    
        ePolicy.setInitialInterval(2000l);//重试间隔时间,2秒
    
        ePolicy.setMaxInterval(30 * 60 * 1000l);//最大重试间隔时间为30分钟
    
        retryTemplate.setRetryPolicy(policy);
    
        retryTemplate.setBackOffPolicy(ePolicy);
    
        final RetryCallback<Object, Exception> retryCallback = new RetryCallback<Object, Exception>() {
    
            public Object doWithRetry(RetryContext context) throws Exception {
    
                logger.info(LocalTime.now() + "do some thing,第" + context.getRetryCount() + "次重试");
    
                //设置context一些属性,给RecoveryCallback传递一些属性
    
                context.setAttribute("key1", "value1");
    
                throw new Exception("RPC调用异常");
    
            }
    
        };
    
    
    
        // 如果RetryCallback执行出现指定异常, 并且超过最大重试次数依旧出现指定异常的话,就执行RecoveryCallback动作
    
        final RecoveryCallback<Object> recoveryCallback = new RecoveryCallback<Object>() {
    
            public Object recover(RetryContext context) throws Exception {
    
                logger.info(context.getLastThrowable().getMessage());
    
                return null;
    
            }
    
        };
    
    
    
        try {
    
            final Object execute = retryTemplate.execute(retryCallback, recoveryCallback);
    
        } catch (Exception e) {
    
            e.printStackTrace();
    
        }
    
    }


    说明:

    1. 设置delay、maxDealy、multiplier,使用 ExponentialBackOffPolicy(指数级重试间隔的实现 ),multiplier即指定延迟倍数,比如delay=5000l,multiplier=2,则第一次重试为5秒,第二次为10秒,第三次为20秒……

    2.通过上述方式可以获取到重试的上下文RetryContext,包含三个参数count,lastException,exhausted,可以看到上面代码中的红色部分。比如:[RetryContext: count=3, lastException=java.lang.RuntimeException: 当前步骤没有定义执行代理, exhausted=false]

    运行结果如下:

    2018-11-07 18:06:19.118  INFO 7776 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : 18:06:19.118do some thing,第0次重试
    2018-11-07 18:06:21.119  INFO 7776 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : 18:06:21.119do some thing,第1次重试
    2018-11-07 18:06:25.120  INFO 7776 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : 18:06:25.120do some thing,第2次重试
    2018-11-07 18:06:25.120  INFO 7776 --- [nio-8080-exec-1] c.e.demo.service.impl.RetryServiceImpl   : RPC调用异常
    类方法简介
    5.1 spring-retry 结构

    5.2 概览

     
    RetryCallback: 封装你需要重试的业务逻辑(上文中的doSth)

     
    RecoverCallback:封装在多次重试都失败后你需要执行的业务逻辑(上文中的doSthWhenStillFail)

     
    RetryContext: 重试语境下的上下文,可用于在多次Retry或者Retry 和Recover之间传递参数或状态(在多次doSth或者doSth与doSthWhenStillFail之间传递参数)

     
    RetryOperations : 定义了“重试”的基本框架(模板),要求传入RetryCallback,可选传入RecoveryCallback;

     
    RetryListener:典型的“监听者”,在重试的不同阶段通知“监听者”(例如doSth,wait等阶段时通知)

     
    RetryPolicy : 重试的策略或条件,可以简单的进行多次重试,可以是指定超时时间进行重试(上文中的someCondition)

     
    BackOffPolicy: 重试的回退策略,在业务逻辑执行发生异常时。如果需要重试,我们可能需要等一段时间(可能服务器过于繁忙,如果一直不间隔重试可能拖垮服务器),
    当然这段时间可以是 0,也可以是固定的,可以是随机的(参见tcp的拥塞控制算法中的回退策略)。回退策略在上文中体现为wait();

    RetryTemplate: RetryOperations的具体实现,组合了RetryListener[],BackOffPolicy,RetryPolicy。
    5.3 重试策略
     
    NeverRetryPolicy:只允许调用RetryCallback一次,不允许重试

     
    AlwaysRetryPolicy:允许无限重试,直到成功,此方式逻辑不当会导致死循环

     
    SimpleRetryPolicy:固定次数重试策略,默认重试最大次数为3次,RetryTemplate默认使用的策略

     
    TimeoutRetryPolicy:超时时间重试策略,默认超时时间为1秒,在指定的超时时间内允许重试

     
    ExceptionClassifierRetryPolicy:设置不同异常的重试策略,类似组合重试策略,区别在于这里只区分不同异常的重试

     
    CircuitBreakerRetryPolicy:有熔断功能的重试策略,需设置3个参数openTimeout、resetTimeout和delegate

    CompositeRetryPolicy:组合重试策略,有两种组合方式,乐观组合重试策略是指只要有一个策略允许重试即可以,
    悲观组合重试策略是指只要有一个策略不允许重试即可以,但不管哪种组合方式,组合中的每一个策略都会执行
    5.4 重试回退策略
    重试回退策略,指的是每次重试是立即重试还是等待一段时间后重试。

    默认情况下是立即重试,如果需要配置等待一段时间后重试则需要指定回退策略BackoffRetryPolicy。

     
    NoBackOffPolicy:无退避算法策略,每次重试时立即重试

     
    FixedBackOffPolicy:固定时间的退避策略,需设置参数sleeper和backOffPeriod,sleeper指定等待策略,默认是Thread.sleep,即线程休眠,backOffPeriod指定休眠时间,默认1秒

     
    UniformRandomBackOffPolicy:随机时间退避策略,需设置sleeper、minBackOffPeriod和maxBackOffPeriod,该策略在[minBackOffPeriod,maxBackOffPeriod之间取一个随机休眠时间,minBackOffPeriod默认500毫秒,maxBackOffPeriod默认1500毫秒

     
    ExponentialBackOffPolicy:指数退避策略,需设置参数sleeper、initialInterval、maxInterval和multiplier,initialInterval指定初始休眠时间,默认100毫秒,maxInterval指定最大休眠时间,默认30秒,multiplier指定乘数,即下一次休眠时间为当前休眠时间*multiplier

    ExponentialRandomBackOffPolicy:随机指数退避策略,引入随机乘数可以实现随机乘数回退
     

    深入探索
    1、spring-retry通过AOP实现对目的方法的封装,执行在当前线程下,所以重试过程中当前线程会堵塞。如果BackOff时间设置比较长,最好起异步线程重试(也可以加@Async注解)。

    参考文章:

    http://www.mamicode.com/info-detail-2051957.html

    http://www.cnblogs.com/jtlgb/p/6813164.html

    spring retry 官网  https://github.com/spring-projects/spring-retry

    重试与融断机制  http://www.broadview.com.cn/article/233


  • 相关阅读:
    attr与prop
    Django框架学习
    库的操作
    javascript 基础知识
    进程
    正则表达式
    模块( collections , time , random , os , sys)
    内置函数
    生成器
    迭代器
  • 原文地址:https://www.cnblogs.com/lywJ/p/10730932.html
Copyright © 2020-2023  润新知