接口重试机制实战

目录

前言

Spring-Retry(重试机制)

一、spring-retry是什么?

二、引入依赖

三、启用@Retryable 

四、在方法上添加@Retryable 

五、单元测试

六、@Retryable注解中参数的含义:

Guava Retry 

一、guava-retrying 是什么

二、引入依赖

三、构建retryer 

四、 主逻辑放在callable里,传给retryer进行调用

五、guava-retrying支持多种条件下重试,来具体看看。

retryIfException()

retryIfRuntimeException()

retryIfExceptionOfType


前言

当我们单体应用时,所有的逻辑计算都在单一的进程中,除了进程断电外几乎不可能有处理失败的情况。然而,当我们把单体应用拆分为一个个细分的子服务后,服务间的互相调用无论是RPC还是HTTP,都是依赖于网络。

网络是脆弱的,不时请求会出现抖动失败。例如我们的 网关Gateway 调用 订单微服务 进行下单时,可能网络超时了,这个时候 网关Gateway 就需要返回给用户提示「网络错误」,这样我们的服务质量就下降了,可能会收到用户的投诉吐槽,降低产品竞争力。

对于网络抖动这种情况,解决的最简单办法之一就是重试


Spring-Retry(重试机制)


在实际工作中,重处理是一个非常常见的场景,比如:

发送消息失败,保存Redis失败。
调用远程服务失败。
支付成功回调失败。
这些错误可能是因为网络波动造成的,等待过后重处理就能成功。通常来说,会用try/catch,while循环之类的语法来进行重处理,但是这样的做法缺乏统一性,会侵入业务代码,难以维护。spring-retry却可以通过注解,在不入侵原有业务逻辑代码的方式下,优雅的实现重处理功能,使业务代码和重处理解耦。

一、spring-retry是什么?


spring系列的spring-retry是另一个实用程序模块,可以帮助我们以标准方式处理任何特定操作的重试。在spring-retry中,所有配置都是基于注解的。
 

二、引入依赖

基于AOP实现需引入aop相关的依赖

         
            org.springframework.retry
            spring-retry
            1.2.4.RELEASE
        
        
            org.springframework.boot
            spring-boot-starter-aop
            2.3.4.RELEASE
        

三、启用@Retryable 

/**
 * 启动类
 *
 * @author yangyanping
 * @date 2022-11-17
 */
@Slf4j
@EnableRetry
@EnableDynamicConfigEvent
@EnableTransactionManagement
@SpringBootApplication(exclude = {DataSourceAutoConfiguration.class})
public class WebApplication {
    public static void main(String[] args) {
        SpringApplication.run(WebApplication.class, args);

        log.info(">>>>>>>>>>>workId={}", System.getProperty("workId"));
    }
}

四、在方法上添加@Retryable 

定义接口UserService

public interface UserService {
    String getUserInfo(String userId);
}

定义接口实现类 UserServiceImpl ,使用@Retryable

@Slf4j
@Service
public class UserServiceImpl implements UserService {
    /**
     * value:抛出指定异常才会重试
     * include:和value一样,默认为空,当exclude也为空时,默认所有异常
     * exclude:指定不处理的异常
     * maxAttempts:最大重试次数,默认3次
     * backoff:重试等待策略,
     * 默认使用@Backoff,@Backoff的value默认为1000L,我们设置为2000; 以毫秒为单位的延迟(默认 1000)
     * multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒。
     */
    @Override
    @Retryable(value = Exception.class, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 1.5))
    public String getUserInfo(String userId) {
        log.info("getUserInfo#userId={}", userId);

        throw ExceptionFactory.bizException("-1", "接口异常");
    }

    /**
     * Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。
     * 如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
     * 可以看到传参里面写的是 Exception e,这个是作为回调的接头暗号(重试次数用完了,还是失败,我们抛出这个Exception e通知触发这个回调方法)。
     * 注意事项:
     * 方法的返回值必须与@Retryable方法一致
     * 方法的第一个参数,必须是Throwable类型的,建议是与@Retryable配置的异常一致,其他的参数,需要哪个参数,写进去就可以了(@Recover方法中有的)
     * 该回调方法与重试方法写在同一个实现类里面
     * 

* 由于是基于AOP实现,所以不支持类里自调用方法 * 如果重试失败需要给@Recover注解的方法做后续处理,那这个重试的方法不能有返回值,只能是void * 方法内不能使用try catch,只能往外抛异常 * * @param e * @param userId * @return * @Recover注解来开启重试失败后调用的方法(注意,需跟重处理方法在同一个类中),此注解注释的方法参数一定要是@Retryable抛出的异常,否则无法识别,可以在该方法中进行日志处理。 */ @Recover public String recover(Exception e, String userId) { log.info("回调方法执行,recover#userId={}",userId); //记日志到数据库 或者调用其余的方法 log.info("异常信息:" + e.getMessage()); return userId; } }

五、单元测试

@Slf4j
@RunWith(SpringRunner.class)
@SpringBootTest(classes = WebApplication.class)
public class BaseTest {

    @Resource
    private UserService userService;

    @Test
    public void getUserInfo(){
        userService.getUserInfo("123");
    }
}

运行结果:

[2023/07/26 20:21:13.948][INFO][UserServiceImpl:25] getUserInfo#userId=123
[2023/07/26 20:21:15.950][INFO][UserServiceImpl:25] getUserInfo#userId=123
[2023/07/26 20:21:18.955][INFO][UserServiceImpl:25] getUserInfo#userId=123
[2023/07/26 20:21:18.958][INFO][UserServiceImpl:50] 回调方法执行,recover#userId=123
[2023/07/26 20:21:18.958][INFO][UserServiceImpl:52] 异常信息:接口异常

六、@Retryable注解中参数的含义:

从中挑出一些重要的属性说明

属性名称 说明
interceptor 重试方法使用的重试拦截器bean名称,和其他的属性互斥(哪个优先待确认)
value 哪些异常可以触发重试 ,是include的同义词,复制将会应用到include,默认为空
include 哪些异常可以触发重试 ,默认为空
exclude 哪些异常将不会触发重试,默认为空,如果和include属性同时为空,则所有的异常都将会触发重试
stateful 是否是无状态
maxAttempts 重试策略之最大尝试次数,默认3次
maxAttemptsExpression 字面意思是使用表达式来提供最大重试次数,默认3次
backoff 重试等待策略,默认使用@Backoff,@Backoff的value默认为1000(单位毫秒),我们设置为2000;multiplier(指定延迟倍数)默认为0,表示固定暂停1秒后进行重试,如果把multiplier设置为1.5,则第一次重试为2秒,第二次为3秒,第三次为4.5秒

Spring-Retry还提供了@Recover注解,用于@Retryable重试失败后处理方法。如果不需要回调方法,可以直接不写回调方法,那么实现的效果是,重试次数完了后,如果还是没成功没符合业务判断,就抛出异常。
 

    @Recover
    public String recover(Exception e, String userId) {
        log.info("回调方法执行,recover#userId={}",userId);
        //记日志到数据库 或者调用其余的方法
        log.info("异常信息:" + e.getMessage());

        return userId;
    }

Guava Retry 

一、guava-retrying 是什么

guava-retrying是Google Guava库的一个扩展包,可以为任意函数调用创建可配置的重试机制。该扩展包比较简单,大约包含了10个方法和类

二、引入依赖

         
            com.github.rholder
            guava-retrying
            2.0.0
        

三、构建retryer 

Retryer是最核心的类,是用于执行重试策略的类,通过RetryerBuilder类进行构造,并且RetryerBuilder负责将设置好的重试策咯添加到Retryer中,最终通过执行Retryer的核心方法call来执行重试策略(一次任务的执行是如何进行的?)

Retryer retryer = RetryerBuilder.newBuilder()
            .retryIfException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(5))
            .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
            .build();

四、 主逻辑放在callable里,传给retryer进行调用

public class RetryTest {
    public static void main(String[] args) {
        mock();
    }

    private static Retryer retryer = RetryerBuilder.newBuilder()
            .retryIfException()
            .withStopStrategy(StopStrategies.stopAfterAttempt(5))
            .withWaitStrategy(WaitStrategies.fixedWait(3, TimeUnit.SECONDS))
            .build();

    public static int mock() {
        Callable callable =() -> doQuery();

        int result;
        try {
            result = retryer.call(callable);
        } catch (Exception e) {
            result = -1;
        }
        return result;
    }

    private static int doQuery() {
        Random r = new Random(System.currentTimeMillis());
        int num = r.nextInt(5);
        System.out.println("query result " + num);

        if (num == 0) {
            return 0;
        } else if (num == 1) {
            System.out.println("DBException");
            throw ExceptionFactory.bizException("-1", "bizException");
        } else if (num == 2) {
            System.out.println("IllegalArgumentException");
            throw new IllegalArgumentException("IllegalArgumentException");
        } else if (num == 3) {
            System.out.println("NullPointerException");
            throw new NullPointerException("NullPointerException");
        } else {
            System.out.println("IndexOutOfBoundsException");
            throw new IndexOutOfBoundsException("IndexOutOfBoundsException");
        }
    }
}

运行结果

query result 3
NullPointerException
query result 0

进程已结束,退出代码0

五、guava-retrying支持多种条件下重试,来具体看看。

retryIfException()

这个就是在任何异常发生时,都会进行重试。上面的例子中已经用到。

retryIfRuntimeException()

这个是指,只有runtime exception发生时,才会进行重试。

retryIfExceptionOfType

发生某种指定异常时,才重试。例如

.retryIfExceptionOfType(DBException.class)

你可能感兴趣的:(分布式,缓存,分布式,微服务,spring,boot)