SpringBoot4新特性-容错和重试

48 阅读6分钟

SpringBoot4 引入了内置的容错功能,通过 @Retryable 和 @ConcurrencyLimit 注解,无需再单独引入 spring-retry 模块即可实现重试和并发控制能力。

分布式应用程序复杂性不断增加,系统的健壮性和可靠性成为了开发者面临的重要挑战,网络波动、外部服务暂时不可用、数据库连接超时等问题在生产环境中屡见不鲜。为了应对这些挑战,开发者通常需要实现重试机制(Retry Mechanism)、并发控制(Concurrency Control)、熔断降级(Circuit Breaker)等容错设计来提高系统的容错能力。提供系统的健壮性。

SpringBoot4 基于Spring Framework7.0 。 通过声明式注解配置即可实现复杂的重试策略,无需手写繁琐的处理逻辑,代码简洁优雅且功能强大。

主要新增的注解包括:

  • @Retryable:方法重试功能(Method Retry)
  • @ConcurrencyLimit:并发限制功能(Concurrency Throttling)

2.1 默认重试

@Retryable,它指定了单个方法(在方法级别声明注释)或给定类中所有代理调用方法(在类级别声明注解的)的重试特性。

示例代码:

 @Retryable
 public String queryOrder() {
     System.out.println(">>>>>>>查询订单queryOrder()方法执行次数:" + (index++) );
 ​
     String  orderUrl = "http://localhost:8080/api/orders?orderId=A2025001";
     RestTemplate restTemplate = new RestTemplate();
     Order order  = restTemplate.getForObject(orderUrl,Order.class);
 ​
     return "HTTP服务查询订单:" + order;
 }

默认重试策略:

  • 对于抛出的任何异常都执行重试
  • 在初始失败后最多重试3次(重试第1次 + 3次重试)
  • 两次尝试之间延迟1秒(固定间隔时间)

完整实现:

1.创建提供者Web应用( boot-04-retry ),接口功能查询订单

创建基于Spring Boot 4的Web应用, 订单类定义如下:

 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 public class Order {
     private String id;
     private String name;
 }

订单查询接口

 @RestController
 public class OrderController {
 ​
     @GetMapping(value = "/api/orders")
     public ResponseEntity<Order> queryOrderById(String orderId){
         Order order = new Order(orderId,"杭州3日自由行");
         try{
             Thread.sleep(500);
             throw new RuntimeException("查询异常");
         }catch (Exception e){
             e.printStackTrace();
             return ResponseEntity.internalServerError().body(order);
         }
     }
 }

订单查询功能,500毫秒后抛出异常。

2.调用服务

创建基于Spring Boot4 Web应用( boot-04-client )。 依赖只需要spring-boot-starter-web ,不需要spring-retry

作为接口的订单类

 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 public class Order {
     private String id;
     private String name;
 }

订单查询服务类

 @Service
 public class QueryOrderService {
 ​
     private int index = 1;
 ​
     @Retryable
     public String queryOrder() {
         System.out.println(">>>>>>>查询订单queryOrder()方法执行次数:" + (index++) );
 ​
         String  orderUrl = "http://localhost:8080/api/orders?orderId=A2025001";
         RestTemplate restTemplate = new RestTemplate();
         Order order  = restTemplate.getForObject(orderUrl,Order.class);
 ​
         return "HTTP服务查询订单:" + order;
     }
 }

查询入口Controller

 @RestController
 public class ConsumerClient {
 ​
     @Resource
     private QueryOrderService queryOrderService;
     
     @GetMapping("/client")
     public String queryOrder()
     {
         return queryOrderService.queryOrder();
     }
 ​
 }

配置声明 @EnableResilientMethods

 @EnableResilientMethods
 @SpringBootApplication
 public class Boot04ClientApplication {
 ​
     public static void main(String[] args) {
         SpringApplication.run(Boot04ClientApplication.class, args);
     }
 ​
 }

3.测试重试

启动提供者boot-04-retry

启动客户端服务boot-04-client

Postman发送GET http://localhost:8000/client

查看两个应用的控制台

客户端服务boot-04-client控制台:

 >>>>>>>查询订单queryOrder()方法执行次数:1
 >>>>>>>查询订单queryOrder()方法执行次数:2
 >>>>>>>查询订单queryOrder()方法执行次数:3
 >>>>>>>查询订单queryOrder()方法执行次数:4

动提供者控制台

 java.lang.RuntimeException: 查询异常
     at com.bjpowernode.controller.OrderController.queryOrderById(OrderController.java:17)
     at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
     at java.base/java.lang.reflect.Method.invoke(Method.java:565)
     at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:256)
     at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)
     ...
 java.lang.RuntimeException: 查询异常
     at com.bjpowernode.controller.OrderController.queryOrderById(OrderController.java:17)
     at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
     at java.base/java.lang.reflect.Method.invoke(Method.java:565)
     at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:256)
     at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)
     ...
 java.lang.RuntimeException: 查询异常
     at com.bjpowernode.controller.OrderController.queryOrderById(OrderController.java:17)
     at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
     at java.base/java.lang.reflect.Method.invoke(Method.java:565)
     at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:256)
     at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)
     ...
 java.lang.RuntimeException: 查询异常
     at com.bjpowernode.controller.OrderController.queryOrderById(OrderController.java:17)
     at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
     at java.base/java.lang.reflect.Method.invoke(Method.java:565)
     at org.springframework.web.method.support.InvocableHandlerMethod.doInvoke(InvocableHandlerMethod.java:256)
     at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:188)  
     ...

2.2 指定重试的异常类型

对特定的异常应用重试, 而不是所有异常

 @Retryable( { NullPointerException.class} )
 public String queryOrder() {
     .......
 }

2.3 重试策略配置

修改重试方法

 ​
 @Service
 public class QueryOrderService {
 ​
     private int index = 1;
     private long pre =0;
     private long cur = 0;
 ​
 ​
     @Retryable( maxAttempts = 5,  // 最大重试次数
             delay = 100,          // 初始延迟(毫秒)
             jitter = 10,          // 随机抖动(Jitter),避免重试风暴
             multiplier = 2,       // 指数退避倍数
             maxDelay = 1000       // 最大延迟上限
     )
     public String queryOrder() {
         System.out.println( System.currentTimeMillis() + ">>>>>>>查询订单queryOrder()方法执行次数:" + (index++) );
 ​
         cur = System.currentTimeMillis();
         if( pre == 0 ){
             pre = cur;
         } else {
             long diff = cur - pre - 1000;
             System.out.println("diff = " + diff);
             pre = cur;
         }
 ​
         try {
             TimeUnit.MILLISECONDS.sleep(1000);
             int x  = 1/0;
         } catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
 ​
         return "HTTP服务查询订单:";
     }
 ​
 }

重试策略:

maxAttempts = 5:最大重试次数为5次, 第一次请求后, 最多重试5次 delay = 100:初始延迟时间为100毫秒,第一次重试会在100ms后进行 jitter = 10:随机抖动值为10毫秒,实际延迟会在基础延迟上增加一个随机值(通常在±10ms范围内) multiplier = 2:指数退避倍数为2,每次重试的延迟时间会翻倍 maxDelay = 1000:最大延迟上限为1000毫秒(1秒),即使指数退避计算出更长的延迟时间,也不会超过这个值

延迟时间计算示例 基于这些参数,重试的延迟时间大致如下: 第1次重试:~100ms 第2次重试:~200ms 第3次重试:~400ms 第4次重试:~800ms 第5次重试:~1000ms(不超过最大延迟1000ms)

这种配置可以有效避免系统过载,同时通过指数退避和随机抖动来分散重试请求,防止重试风暴。

执行重试方法

 1757753468129>>>>>>>查询订单queryOrder()方法执行次数:1
 1757753469242>>>>>>>查询订单queryOrder()方法执行次数:2
 diff = 108
 1757753470432>>>>>>>查询订单queryOrder()方法执行次数:3
 diff = 190
 1757753471862>>>>>>>查询订单queryOrder()方法执行次数:4
 diff = 430
 1757753473737>>>>>>>查询订单queryOrder()方法执行次数:5
 diff = 875
 1757753475721>>>>>>>查询订单queryOrder()方法执行次数:6
 diff = 984

2.4 @ConcurrencyLimit 并发控制

@ConcurrencyLimit 注解用于实现并发限流,限制方法的并发访问数量。 类级别和方法级别可用。

默认并发限制为 1 。

限制访问的方法

 @RestController
 public class LimitController {
     
     //至多10个并发请求 @ConcurrencyLimit(value = 1)
     @ConcurrencyLimit(value = 1)
     @GetMapping("/hello")
     public String hello(){
         try {
             Thread.sleep(200);
         } catch (InterruptedException e) {
             throw new RuntimeException(e);
         }
         return "hello world";
     }
 }

设置限制为 1 实现对方法的独占访问, 确保线程安全 , 方法调用为串行执行,

配置类

 @EnableResilientMethods
 @SpringBootApplication
 public class Boot05ConcurrencyLimitApplication {
 ​
     public static void main(String[] args) {
         SpringApplication.run(Boot05ConcurrencyLimitApplication.class, args);
     }
 ​
 }

单元测试方法

 @SpringBootTest
 class Boot05ConcurrencyLimitApplicationTests {
 ​
 ​
     private RestTemplate restTemplate = new RestTemplate();
 ​
     @Test
     void testConcurrencyTest() {
         ExecutorService executorService = Executors.newFixedThreadPool(15);
 ​
         List<Future<ResponseEntity<String>>>  futures = new ArrayList<>();
 ​
         for (int i = 0; i < 20; i++) {
             Future<ResponseEntity<String>> future = executorService.submit(() -> {
                 return restTemplate.getForEntity("http://localhost:8080/hello", String.class);
             });
             futures.add(future);
         }
 ​
         int successCount = 0;
         int failureCount  = 0;
 ​
         for (Future<ResponseEntity<String>> future : futures) {
             try {
                 ResponseEntity<String> response = future.get(200, TimeUnit.MILLISECONDS);
                 if (response.getStatusCode().is2xxSuccessful()) {
                     successCount++;
                 } else {
                     System.out.println("失败:" + response.getStatusCode());
                     failureCount++;
                 }
             } catch (Exception e) {
                 e.printStackTrace();
                 failureCount++;
                 future.cancel(true);
             }
         }
 ​
         executorService.shutdown();
 ​
         System.out.println("成功:" + successCount);
         System.out.println("失败:" + failureCount);
 ​
     }
 }

访问超时200描述, 设置并发访问1 (尝试修改其他,比如10), 单元测试部分请求成功,部分失败。异常为超时错误。

 ​
 同步调用拦截器: ConcurrencyThrottleInterceptor
 异步调用:SimpleAsyncTaskExecutor