Spring Boot 2.x 实践记:Retry(annotion)

Posted mickjoust

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Spring Boot 2.x 实践记:Retry(annotion)相关的知识,希望对你有一定的参考价值。

目录

  • 1、为什么要重试(retry)?
  • 2、spring-retry
  • 3、实战
  • 4、测试
  • 5、小结

1. 为什么需要重试(retry)?

在我们的Web项目中,通常会遇到一些场景,导致业务获取结果异常,比如:

  • 网络中断
  • 网络超时
  • 服务器宕机
  • 数据库崩溃
  • 程序死锁
  • 秒杀大促

在这些情况下,故障是一定会发生的,而我们的系统能否继续为用户提供服务是关键中的关键。

重试机制,是解决这类问题的一个大的策略和原则。

重试的原理很简单:当一次请求发生异常时,间隔一定时间后,重新继续请求,直到达到设置的重试次数用完后,才认为请求出现异常。

随着微服务、云原生应用的增多,服务之间交互的情况也越来越多,重试就变得越来越重要。

熟悉Spring Cloud源码的同学应该知道,spring-cloud-commons依赖了一个spring-retry模块。看下官方 定义:

  • 该项目为Spring应用程序提供了声明式重试支持。它用于Spring Batch,Spring Integration等项目。显式用法也支持命令重试。

简单说,就是用Spring的风格实现的一种重试最佳实践。

所以,今天,我们就借助spring-retry模块,来实战一下重试方案。

2. spring-retry

spring-retry模块中,有三个关键注解:

  • @EnableRetry:在spring boot项目中启用spring-retry功能
  • @Retryable:表示需要重试的方法,有相关参数支持
  • @Recover:表示重试失败后指定运行的方法

3. 实战

3.1. 场景概述

  1. 创建一个Spring Boot 2项目,提供Rest API服务;
  2. 服务包含两个可测试开关:一个是否开启异常重试,一个开启重试后是否直接触发异常;
  3. 重试次数设置为3次,间隔1000ms,服务调用成功异常后打印字符串。

3.2. 环境配置

  • JDK:open-jdk-8
  • Maven:3.x
  • spring-boot:2.x
  • spring-retry:1.x
  • spring-aspects: 5.x(spring-retry 的 AOP支持)
  • spring-context:5.x
  • Postman:测试工具

3.3. 创建Spring boot项目

第一步,我们需要创建一个Spring Boot 2项目,一般使用自动生成的方法。

点击网站:https://start.spring.io/

选择好版本后,下载包含框架项目的zip文件,使用IDE打开即可。

3.4. Maven依赖

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<version>2.2.6.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework.retry</groupId>
    <artifactId>spring-retry</artifactId>
    <version>1.2.5.RELEASE</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.2.5.RELEASE</version>
</dependency>

3.5. @EnableRetry

为了使用spring-retry功能,我们需要在启动类中加入放置@EnableRetry。

先创建启动类 AppStart.java,增加 @SpringBootApplication ,然后添加 @EnableRetry 即可。

package com.mickjoust.demo.springboot2_in_action.retry;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;

/**
 * @author mickjoust
 **/
@EnableRetry
@SpringBootApplication
public class AppStart 
    public static void main(String[] args) 
        SpringApplication.run(AppStart.class,args);
    

3.6. 创建Restful API

创建一个Rest控制器,该控制器用于调用后端服务类,在该类中我们将模拟异常,并且spring-retry模块将自动重试。

package com.mickjoust.demo.springboot2_in_action.retry.annotion;

import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;

/**
 * @author mickjoust
 **/
@RestController
public class TestRestController 

    @Resource
    private BackendService backendService;

    @GetMapping("/retry")
    @ExceptionHandler( Exception.class )
    public String doRetryTest(@RequestParam(required = false) boolean openretry,
                              @RequestParam(required = false) boolean openfallback) 
        System.out.println("===============================");

        return backendService.service(openretry, openfallback);
    



在API中,我们添加了两个可选的请求参数作为模拟异常的开关。

  • openretry:模拟异常的开关,以便spring-retry可以重试。
  • openfallback:模拟每次请求必然遇见异常。

3.7. 定义异常

spring-retry在重试时需要捕获异常进行处理,这里我们先定义一个简单的运行时异常,模拟服务不可用的异常。

package com.mickjoust.demo.springboot2_in_action.retry;

/**
 * @author mickjoust
 **/
public class BackendNotAvailableException extends RuntimeException 

    public BackendNotAvailableException(String message) 
        super(message);
    

    public BackendNotAvailableException(String message, Throwable cause) 
        super(message, cause);
    


3.8. 实现单服务接口

现在,我们需要创建一个用于调用的后台服务接口和实现。

案例里,并没有调用任何真实的外部服务调用,只是通过添加一些随机逻辑来模拟成功或失败。

package com.mickjoust.demo.springboot2_in_action.retry.annotion;

import com.mickjoust.demo.springboot2_in_action.retry.BackendNotAvailableException;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;

/**
 * @author mickjoust
 **/
public interface BackendService 

    @Retryable(value = BackendNotAvailableException.class ,
            maxAttempts = 3,
            backoff = @Backoff(delay = 1000))
    String service(boolean openretry, boolean openfallback);

    @Recover
    String serviceFallback(BackendNotAvailableException e);


package com.mickjoust.demo.springboot2_in_action.retry.annotion;

import com.mickjoust.demo.springboot2_in_action.retry.BackendNotAvailableException;
import org.springframework.stereotype.Service;

import java.util.Random;

/**
 * @author mickjoust
 **/
@Service("backendService")
public class BackendServiceImpl implements BackendService

    private static int count = 0;
    
    @Override
    public String service(boolean openretry, boolean openfallback) 
        if (openretry) 
            System.out.println("模拟开关已打开,开始模拟异常......");

            if (openfallback) 
                count++;
                System.out.println("模拟直接异常开关已打开,直接进行重试......"+count+"次");
                throw new BackendNotAvailableException(
                        "抛出异常,spring-retry进行重试!");
            

            System.out.println("......模拟随机异常(是否整除2)......");
            int random = new Random().nextInt(4);

            System.out.println("随机数为 : " + random);
            if (random % 2 == 0) 
                count++;
                System.out.println("出现随机异常......进行重试......"+count+"次");
                throw new BackendNotAvailableException("抛出异常,spring-retry进行重试!");
            
        
        System.out.println("调用正常!");
        count=0;
        return "Hello Service!";
    

    @Override
    public String serviceFallback(BackendNotAvailableException e) 
        System.out.println("所有重试已完成, 调用serviceFallback方法!!!");
        count=0;
        return "所有重试已完成, 调用serviceFallback方法!!!";
    


说明:

  • @Retryable:这个注释是说,如果我们的接口方法抛出RemoteServiceNotAvailableException 异常,则在返回响应之前最多重试3次,同时,每次重试都会间隔1秒的延迟。
  • @Recover:如果达到最大重试次数后,依然出现异常,则使用此方法返回响应。

前面说过,在调用后台服务的实际方法中,我们是通过添加自定义开关参数来控制Exception的。

代码逻辑很简单,只要满足条件就返回期望的异常并触发充实,否则返回成功响应。

另外,我们还基于“随机数”添加了一些随机逻辑,以模拟故障的随机性。

4. 测试

通过使用postman,在REST请求中传递参数来模拟重试请求。

4.1. 场景1:直接异常重试

输入请求如下:

http://localhost:8080/retry?openretry=true&openfallback=true

期望结果:

  • 基于该参数,我们期望后端服务调用中直接出现异常,尝试完3次后,返回备用方法的返回。

    在日志打印中,能看到尝试了3次重试都触发异常后,返回备用方法打印。

4.2. 场景2:随机异常重试

输入请求如下:

http://localhost:8080/retry?openretry=true&openfallback=false

期望结果:

  • 可能直接调用成功,可能在重试范围内成功,可能是用完重试次数返回备用方法的结果。

我们通过获取随机数,来模拟异常,同样能够出发重试或者直接成功。

5. 小结

到此,我们已经知道了如何使用 spring-retry 模块,轻松实现基于异常Exception的重试的办法。

所以,如果下次遇到需要重试请求的场景,可以尝试使用这种方法。

欢迎留言评论。

参考资源

以上是关于Spring Boot 2.x 实践记:Retry(annotion)的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot 2.x 实践记:Retry(annotion)

(汇总)Spring Boot 实践折腾记 & Spring Boot 2.x 实践记

Spring Boot 2.x 实践记:Gson

Spring Boot 2.x 实践记:Mail

Spring Boot 2.x 实践记:Mail

Spring Boot 2.x 实践记:Mail