问题背景
在现代的分布式系统中,服务间的调用往往需要处理各种网络异常、超时等问题。重试机制是一种常见的解决策略,它允许应用程序在网络故障或临时性错误后自动重新尝试失败的操作。
Spring Boot
提供了灵活的方式来集成重试机制,这可以通过使用Spring Retry
模块来实现。本文将通过一个具体的使用场景来详细介绍如何在Spring Boot
应用中集成和使用Spring Retry
技术。
场景描述
假如我们正在开发一个OMS
系统(订单管理系统),其中一个关键服务 OrderService
负责订单创建和调用WMS
服务扣减库存API 。由于网络不稳定或外部 API 可能暂时不可用,我们需要在这些情况下实现重试机制,以确保请求的成功率和系统的稳定性。
实现步骤
1. 添加依赖
首先,在你的 pom.xml
文件中添加 Spring Retry
和 AOP
的依赖:
<dependencies><!-- Spring Boot Starter Web --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!-- Spring Retry --><dependency><groupId>org.springframework.retry</groupId><artifactId>spring-retry</artifactId></dependency><!-- Spring Boot Starter AOP --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency></dependencies>
2. 创建配置类
创建一个配置类来配置重试模板:
package com.zlp.retry.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.ExponentialBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;
/*** 全局重试配置*/
@Configuration
public class CustomRetryConfig {/**** 这段代码定义了一个 `CustomRetryConfig` 类,其中包含一个 `retryTemplate` 方法。该方法用于创建并配置一个 `RetryTemplate` 对象,该对象用于处理重试逻辑。** 1. 创建 `RetryTemplate` 实例**:创建一个 `RetryTemplate` 对象。* 2. 设置重试策略:使用 `SimpleRetryPolicy` 设置最大重试次数为5次。* 3. 设置延迟策略:使用 `ExponentialBackOffPolicy` 设置初始延迟时间为1000毫秒,每次重试间隔时间乘以2。* 4. 应用策略:将重试策略和延迟策略应用到 `RetryTemplate` 对象。*/@Beanpublic RetryTemplate retryTemplate() {RetryTemplate template = new RetryTemplate();// 设置重试策略SimpleRetryPolicy policy = new SimpleRetryPolicy();policy.setMaxAttempts(5);// 设置延迟策略ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();backOffPolicy.setInitialInterval(1000);backOffPolicy.setMultiplier(2.0);template.setRetryPolicy(policy);template.setBackOffPolicy(backOffPolicy);return template;}
}
3. 创建服务类
创建一个服务类 OrderService
和接口实现类OrderServiceImpl
,并在需要重试的方法上使用 @Retryable
注解:
/*** @Classname OrderService* @Date 2024/11/18 21:03* @Created by ZouLiPing*/
public interface OrderService {/*** 创建订单* @param createOrderReq* @return*/String createOrder(CreateOrderReq createOrderReq);
}
package com.zlp.retry.service.impl;import cn.hutool.core.date.DateUtil;
import com.alibaba.fastjson.JSON;
import com.zlp.retry.dto.CreateOrderReq;
import com.zlp.retry.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Recover;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;import java.util.UUID;/*** @Classname OrderServiceImpl* @Date 2024/11/18 21:06* @Created by ZouLiPing*/
@Service
@Slf4j(topic = "OrderServiceImpl")
public class OrderServiceImpl implements OrderService {@Override@Retryable(value = {Exception.class},maxAttempts = 4, backoff = @Backoff(delay = 3000))public String createOrder(CreateOrderReq createOrderReq) {log.info("createOrder.req createOrderReq:{}", JSON.toJSONString(createOrderReq));try {log.info("createOrder.deductStock.调用时间={}", DateUtil.formatDateTime(DateUtil.date()));// 扣减库存服务this.deductStock(createOrderReq);} catch (Exception e) {throw new RuntimeException(e);}return UUID.randomUUID().toString();}/*** 模拟扣减库存*/private void deductStock(CreateOrderReq createOrderReq) {throw new RuntimeException("库存扣减失败");}/*** 当重试四次仍未能成功创建订单时调用此方法进行最终处理** @param ex 异常对象,包含重试失败的原因* @param createOrderReq 创建订单的请求对象,包含订单相关信息* @return 返回处理结果,此处返回"fail"表示最终失败*/@Recoverpublic String recover(Exception ex, CreateOrderReq createOrderReq) {// 记录重试四次后仍失败的日志,包括异常信息和订单请求内容log.error("recover.resp.重试四次还是失败.error:{},createOrderReq:{}",ex.getMessage(),JSON.toJSONString(createOrderReq));// 处理最终失败的情况// 可以记录日志,或者是投递MQ,采用最终一致性的方式处理return "fail";}
}
4. 启用重试功能
在主配置类或启动类上添加 @EnableRetry
注解,以启用重试功能:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.retry.annotation.EnableRetry;@SpringBootApplication
@EnableRetry
public class Application {public static void main(String[] args) {SpringApplication.run(Application.class, args);}
}
5. 验证重试方法
@RestController
@RequiredArgsConstructor
public class RetryController {private final OrderService orderService;@GetMapping("getRetry")public String retry(){CreateOrderReq createOrderReq = new CreateOrderReq();createOrderReq.setOrderId(UUID.randomUUID().toString());createOrderReq.setProductId("SKU001");createOrderReq.setCount(10);createOrderReq.setMoney(100);return orderService.createOrder(createOrderReq);}
}
6.执行操作说明
- 添加依赖:引入了 Spring Retry 和 AOP 的依赖,以便使用重试功能。
- 配置重试模板:创建了一个配置类
CustomRetryConfig
,配置了重试策略,设置最大重试次数为5次。 - 创建服务类:在
OrderServiceImpl
类中,使用@Retryable
注解标记了createOrder
方法,指定了当发生Exception
时进行重试,最大重试次数为4
次,每次重试间隔3秒。同时,使用@Recover
注解标记了recover
方法,当所有重试都失败后,会调用这个方法。 - 启用重试功能:在主配置类或启动类上添加
@EnableRetry
注解,以启用重试功能。
Retry执行流程
Retry整体流程图
打印日志
从日志分析每隔3秒钟会重试一次,直到到达设置最大重试次数,会调用功<font style="color:#DF2A3F;">recover</font>
方法中
7.结论
通过以上步骤,我们成功地在 Spring Boot
应用中集成了 Spring Retry
技术,实现了服务调用的重试机制。这不仅提高了系统的健壮性和稳定性,还减少了因网络问题或外部服务暂时不可用导致的请求失败。希望本文对你理解和应用 Spring Boot
中的重试技术有所帮助。
Retry配置的优先级规则
- 方法级别配置:如果某个配置在方法上定义了,则该方法上的配置会覆盖类级别的配置和全局配置。
- 类级别配置:如果某个配置在类上定义了,并且该类的方法没有单独定义配置,则使用类级别的配置。
- 全局配置:如果没有在方法或类上定义配置,则使用全局配置。
下面通过一个具体的例子来展示这些优先级规则。假设我们有一个服务类 <font style="color:rgb(44, 44, 54);">MyService</font>
,其中包含一些方法,并且我们在不同的层次上进行了重试策略的配置。
示例代码
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;@Service
@Retryable(value = {RuntimeException.class},maxAttempts = 3,backoff = @Backoff(delay = 1000) // 类级别的配置
)
public class MyService {@Retryable(value = {RuntimeException.class},maxAttempts = 5,backoff = @Backoff(delay = 500) // 方法级别的配置)public void retryableMethodWithSpecificConfig() {System.out.println("Retrying with specific config...");throw new RuntimeException("Simulated exception");}@Retryable(value = {RuntimeException.class})public void retryableMethodWithoutSpecificDelay() {System.out.println("Retrying without specific delay...");throw new RuntimeException("Simulated exception");}public void nonRetryableMethod() {System.out.println("This method does not retry.");throw new RuntimeException("Simulated exception");}
}
解释
- retryableMethodWithSpecificConfig
- 方法级别配置:
<font style="color:rgb(44, 44, 54);">maxAttempts = 5</font>
<font style="color:rgb(44, 44, 54);">backoff.delay = 500</font>
- 这些配置会覆盖类级别的配置。
- 方法级别配置:
- retryableMethodWithoutSpecificDelay
- 方法级别配置:
<font style="color:rgb(44, 44, 54);">maxAttempts = 3</font>
(继承自类级别)<font style="color:rgb(44, 44, 54);">backoff.delay = 1000</font>
(继承自类级别)
- 这些配置继承自类级别的配置。
- 方法级别配置:
- nonRetryableMethod
- 该方法没有使用
<font style="color:rgb(44, 44, 54);">@Retryable</font>
注解,因此不会进行重试。
- 该方法没有使用
全局配置示例
为了进一步说明全局配置的优先级,我们可以配置一个全局的重试模板。
配置类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.retry.backoff.FixedBackOffPolicy;
import org.springframework.retry.policy.SimpleRetryPolicy;
import org.springframework.retry.support.RetryTemplate;@Configuration
public class RetryConfig {@Beanpublic RetryTemplate retryTemplate() {RetryTemplate template = new RetryTemplate();SimpleRetryPolicy policy = new SimpleRetryPolicy();policy.setMaxAttempts(4);template.setRetryPolicy(policy);FixedBackOffPolicy fixedBackOffPolicy = new FixedBackOffPolicy();fixedBackOffPolicy.setBackOffPeriod(750L);template.setBackOffPolicy(fixedBackOffPolicy);return template;}
}
使用全局配置的服务类
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;@Service
public class AnotherService {@Autowiredprivate RetryTemplate retryTemplate;@Retryable(value = {RuntimeException.class},maxAttempts = 6, // 方法级别的配置backoff = @Backoff(delay = 300) // 方法级别的配置)public void retryableMethodWithGlobalAndLocalConfig() {System.out.println("Retrying with global and local config...");throw new RuntimeException("Simulated exception");}@Retryable(value = {RuntimeException.class})public void retryableMethodWithOnlyGlobalConfig() {System.out.println("Retrying with only global config...");throw new RuntimeException("Simulated exception");}
}
解释
- retryableMethodWithGlobalAndLocalConfig
- 方法级别配置:
<font style="color:rgb(44, 44, 54);">maxAttempts = 6</font>
<font style="color:rgb(44, 44, 54);">backoff.delay = 300</font>
- 这些配置会覆盖全局配置。
- 方法级别配置:
- retryableMethodWithOnlyGlobalConfig
- 全局配置:
<font style="color:rgb(44, 44, 54);">maxAttempts = 4</font>
<font style="color:rgb(44, 44, 54);">backoff.delay = 750</font>
- 这些配置继承自全局配置。
- 全局配置:
总结一下,配置的优先级从高到低依次是:
- 方法级别配置
- 类级别配置
- 全局配置
希望这个示例能帮助你理解不同层次配置的优先级。
什么样的场景不适合Retry
在使用重试机制时,确实有一些场景不适合应用重试策略。了解这些场景有助于避免不必要的重试操作,从而提高系统的性能和稳定性。以下是几种不适合使用重试机制的常见场景:
- 幂等性不可保证的操作
- 解释:如果一个操作不是幂等的(即多次执行会产生不同的结果),那么重试可能导致数据不一致或其他问题。
- 示例:插入数据库记录的操作通常不是幂等的,因为重复插入会导致重复的数据。
- 长时间运行的操作
- 解释:对于耗时较长的操作,频繁重试可能会导致系统资源被大量占用,影响其他任务的执行。
- 示例:批量处理大数据集、长时间计算的任务。
- 外部依赖不稳定但无法恢复
- 解释:某些外部服务或API可能存在根本性的故障,无法通过简单的重试解决。在这种情况下,重试只会浪费资源。
- 示例:调用第三方支付接口,如果返回的是明确的失败状态码(如账户余额不足),则不应该重试。
- 网络超时且无可用备用路径
- 解释:在网络请求超时时,如果没有任何备用路径或解决方案,重试可能仍然会失败。
- 示例:尝试连接到某个特定IP地址的服务,如果该地址一直不通,则重试没有意义。
- 用户交互过程中需要立即反馈的操作
- 解释:在用户等待响应的过程中,长时间的重试可能导致用户体验不佳。
- 示例:提交表单后立即显示成功消息,如果在此期间发生错误并进行重试,用户可能会感到困惑。
- 涉及敏感信息的操作
- 解释:对于涉及敏感信息的操作(如密码修改、资金转账),重试可能会导致敏感信息泄露或重复操作。
- 示例:更新用户的银行账户信息,一旦确认操作完成,不应再进行重试。
- 事务边界内的操作
- 解释:在事务边界内,重试可能会导致事务冲突或回滚,增加复杂性。
- 示例:在一个复杂的数据库事务中,部分操作失败后进行重试可能导致整个事务失败。
- 已知的永久性错误
- 解释:如果能够明确判断出错误是永久性的(如配置错误、代码bug),重试不会解决问题。
- 示例:尝试读取不存在的文件,这种错误通常是永久性的。
- 高并发环境下的写操作
- 解释:在高并发环境下,频繁的重试可能会加剧数据库负载,导致更多的锁竞争和死锁。
- 示例:在电商网站的下单高峰期,对库存的减少操作不宜频繁重试。
示例代码
为了更好地理解这些原则,下面是一个简单的示例,展示了如何在Spring Boot中使用<font style="color:rgb(44, 44, 54);">@Retryable</font>
注解,并根据上述原则决定哪些操作适合重试。
import org.springframework.retry.annotation.Backoff;
import org.springframework.retry.annotation.Retryable;
import org.springframework.stereotype.Service;@Service
public class MyService {// 适合重试的操作:幂等性强、短时间操作、可恢复的错误@Retryable(value = {RuntimeException.class},maxAttempts = 3,backoff = @Backoff(delay = 1000))public void retryableFetchData() {System.out.println("Fetching data...");// 模拟网络请求或短暂的外部服务调用if (Math.random() > 0.5) {throw new RuntimeException("Simulated transient network error");}}// 不适合重试的操作:幂等性不可保证、长时间运行public void nonRetryableLongRunningTask() {System.out.println("Starting long-running task...");try {Thread.sleep(10000); // 模拟长时间运行的任务} catch (InterruptedException e) {Thread.currentThread().interrupt();}System.out.println("Long-running task completed.");}// 不适合重试的操作:涉及敏感信息public void updateSensitiveInformation(String sensitiveData) {System.out.println("Updating sensitive information...");// 这里假设更新操作不是幂等的,也不应该重试throw new RuntimeException("Simulated failure in updating sensitive information");}// 不适合重试的操作:已知的永久性错误public void fetchDataFromNonExistentResource() {System.out.println("Fetching data from a non-existent resource...");throw new RuntimeException("Permanent error: Resource not found");}
}
解释
- retryableFetchData
- 适用条件:
- 幂等性强:每次请求的结果相同。
- 短时间操作:模拟网络请求或短暂的外部服务调用。
- 可恢复的错误:模拟暂时的网络错误。
- 重试策略:
- 最大重试次数为3次。
- 每次重试间隔1秒。
- 适用条件:
- nonRetryableLongRunningTask
- 不适用原因:
- 长时间运行:模拟长时间运行的任务。
- 重试可能导致资源过度消耗。
- 不适用原因:
- updateSensitiveInformation
- 不适用原因:
- 涉及敏感信息:更新操作不是幂等的,也不应该重试。
- 重试可能导致数据不一致或其他安全问题。
- 不适用原因:
- fetchDataFromNonExistentResource
- 不适用原因:
- 已知的永久性错误:资源不存在,重试不会解决问题。
- 不适用原因:
通过这些示例,你可以更好地理解哪些操作适合重试以及为什么某些操作不适合重试。