在当今的数字环境中,应用程序严重依赖外部 HTTP/REST API来实现各种功能。这些 API 通常会编排复杂的内部和外部 API 调用网络。这创建了一个依赖网络。因此,当依赖的 API 发生故障或停机时,面向主要应用程序的 API 需要熟练、优雅地处理这些中断。鉴于此,本文探讨了Spring 微服务中重试机制和回退方法的实现。具体来说,它强调了这些策略如何显着增强 API 集成可靠性并显着改善用户体验。
了解相关 API 故障
使用依赖于其他服务的 API 来成功执行的移动和 Web 应用程序面临着独特的挑战。例如,对依赖 API 的调用可能会因各种原因而失败,包括网络问题、超时、内部服务器错误或计划停机。因此,此类故障可能会损害用户体验、破坏关键功能并导致数据不一致。因此,实施策略来妥善处理这些故障对于维护系统完整性至关重要。
重试机制
作为主要解决方案,重试机制用于处理暂时性错误和临时问题。通过自动重新尝试 API 调用,此机制通常可以解决与短暂网络故障或临时服务器不可用相关的问题。重要的是,区分适合重试的场景(例如网络超时)和重试可能无效甚至有害的场景(例如业务逻辑异常或数据验证错误)至关重要。
重试策略
常见的方法包括:
-
固定间隔重试:定期尝试重试
-
指数退避:此策略涉及以指数方式增加重试之间的间隔,从而减少服务器和网络的负载。
此外,这两种方法都应该附带最大重试限制,以防止无限循环。此外,监控和记录每次重试尝试对于将来的分析和系统优化也很重要。
Spring 微服务中的重试和回退
我们可以通过两种方式来实现重试和回退方法。
1. 弹性4j
@Retry注解是一种声明式方式,旨在简化应用程序中重试逻辑的实现。该注释可在resilience4j 包中找到。通过将此注解应用于服务方法,Spring 在遇到指定类型的异常时自动处理重试过程。
下面是一个真实的实现例子。该方法调用 API 来提取用于承保贷款申请的局报告。如果此方法失败,整个贷款申请承保流程就会失败,从而影响消费应用程序,例如移动应用程序。所以我们用以下注释来注释这个方法@Retry:
@Override
@Retry(name = "SOFT_PULL", fallbackMethod = "performSoftPull_Fallback")
public CreditBureauReportResponse performSoftPull(SoftPullParams softPullParams, ErrorsI error) {
CreditBureauReportResponse result = null;
try {
Date dt = new Date();
logger.info("UnderwritingServiceImpl::performSoftPull method call at :" + dt.toString() + ", for loan acct id:" + softPullParams.getUserLoanAccountId());
CreditBureauReportRequest request = this.getCreditBureauReportRequest(softPullParams);
RestTemplate restTemplate = this.externalApiRestTemplateFactory.getRestTemplate("SOFT_PULL", error);
HttpHeaders headers = this.getHttpHeaders(softPullParams);
HttpEntity<CreditBureauReportRequest> entity = new HttpEntity<>(request, headers);
long startTime = System.currentTimeMillis();
String uwServiceEndPoint = "/transaction";
String callUrl = String.format("%s%s", appConfig.getUnderwritingTransactionApiPrefix(), uwServiceEndPoint);
ResponseEntity<CreditBureauReportResponse> responseEntity = restTemplate.exchange(callUrl, HttpMethod.POST, entity, CreditBureauReportResponse.class);
result = responseEntity.getBody();
long endTime = System.currentTimeMillis();
long timeDifference = endTime - startTime;
logger.info("Time taken for API call SOFT_PULL/performSoftPull call 1: " + timeDifference);
} catch (HttpClientErrorException exception) {
logger.error("HttpClientErrorException occurred while calling SOFT_PULL API, response string: " + exception.getResponseBodyAsString());
throw exception;
} catch (HttpStatusCodeException exception) {
logger.error("HttpStatusCodeException occurred while calling SOFT_PULL API, response string: " + exception.getResponseBodyAsString());
throw exception;
} catch (Exception ex) {
logger.error("Error occurred in performSoftPull. Detail error:", ex);
throw ex;
}
return result;
}
我们可以在application.yml文件中定义其他属性,例如重试次数和重试之间的延迟:
resilience4j.retry:
configs:
default:
maxRetryAttempts: 3
waitDuration: 100
externalPartner:
maxRetryAttempts: 2
waitDuration: 1000
instances:
SOFT_PULL:
baseConfig: externalPartner
我们指定后备方法fallbackMethod = "performSoftPull_Fallback"。如果所有配置的重试尝试均失败,则调用此方法;在这种情况下,是两个。
public CreditBureauReportResponse performSoftPull_Fallback(SoftPullParams softPullParams, ErrorsI error, Exception extPartnerException) {
logger.info("UnderwritingServiceImpl::performSoftPull_Fallback - fallback , method called for soft pull api call");
CreditBureauReportResponse creditBureauReportResponse = null;
String loanAcctId = softPullParams.getUserLoanAccountId();
ApplicantCoBorrowerIdsMapping applicantCoBorrowerIdsMapping = this.uwCoBorrowerRepository.getApplicantCoBorrowerIdsMapping(loanAcctId);
try {
boolean result = this.partnerServiceExceptionRepository.savePartnerServiceException(applicantCoBorrowerIdsMapping.getApplicantUserId(),
applicantCoBorrowerIdsMapping.getLoanId(), PartnerService.SOFT_PULL.getValue(), "GDS", null);
if (!result) {
logger.error("UnderwritingServiceImpl::performSoftPull_Fallback - Unable to save entry in the partner service exception table.");
}
LoanSubStatus loanSubStatus = LoanSubStatus.PARTNER_API_ERROR;
result = this.loanUwRepository.saveLoanStatus(applicantCoBorrowerIdsMapping.getApplicantUserId(), applicantCoBorrowerIdsMapping.getLoanId(),
IcwLoanStatus.INITIATED.getValue(), loanSubStatus.getName(), "Partner Service Down", null);
if (!result) {
logger.error("UnderwritingServiceImpl::performSoftPull_Fallback - Unable to update loan status, sub status when partner service is down.");
}
} catch (Exception ex) {
logger.error("UnderwritingServiceImpl::performSoftPull_Fallback - An error occurred while calling softPullExtPartnerFallbackService, detail error:", ex);
}
creditBureauReportResponse = new CreditBureauReportResponse();
UnderwritingApiError underwritingApiError = new UnderwritingApiError();
underwritingApiError.setCode("IC-EXT-PARTNER-1001");
underwritingApiError.setDescription("Soft Pull API error");
List<UnderwritingApiError> underwritingApiErrors = new ArrayList<>();
underwritingApiErrors.add(underwritingApiError);
creditBureauReportResponse.setErrors(underwritingApiErrors);
return creditBureauReportResponse;
}
在这种情况下,后备方法返回与原始方法相同的响应对象。但是,我们还在数据存储中记录服务已关闭并保存状态,并将指示符转发回消费者服务方法。然后,该指示器会传递到消费移动应用程序,提醒用户有关我们合作伙伴服务的问题。一旦问题得到纠正,我们就会利用持久状态恢复工作流程并向移动应用程序发送通知,表明可以继续正常操作。
2. 弹簧重试
在这种情况下,我们需要安装spring-retry和spring-aspects包。对于与上面相同的方法,我们将其替换为@Retry注解:
@Retryable(retryFor = {HttpClientErrorException.class, HttpStatusCodeException.class, Exception.class}, maxAttempts = 2, backoff = @Backoff(delay = 100))
public CreditBureauReportResponse performSoftPull(SoftPullParams softPullParams, ErrorsI error) {
Spring 中的注释@Retryable允许我们指定应该触发重试的多个异常类型。我们可以在注释的 value 属性中列出这些异常类型。
@Retryable要为带注释的方法编写后备方法performSoftPull,我们将使用@Recover注释。当该方法performSoftPull由于指定的异常 ( HttpClientErrorException, HttpStatusCodeException, Exception) 而耗尽其重试尝试时,将调用此方法。该@Recover方法应该具有与该方法匹配的签名@Retryable,并添加异常类型作为第一个参数。
@Recover
public CreditBureauReportResponse fallbackForPerformSoftPull(HttpClientErrorException ex, SoftPullParams softPullParams, ErrorsI error) {
// Fallback Implementation
}
@Recover
public CreditBureauReportResponse fallbackForPerformSoftPull(HttpStatusCodeException ex, SoftPullParams softPullParams, ErrorsI error) {
// Fallback Implementation
}
@Recover
public CreditBureauReportResponse fallbackForPerformSoftPull(Exception ex, SoftPullParams softPullParams, ErrorsI error) {
// Fallback Implementation
}
结论
总之,在 Spring 微服务中,通过重试机制和回退方法有效处理 API 故障对于构建健壮的、以用户为中心的应用程序至关重要。这些策略确保应用程序保持功能并提供无缝的用户体验,即使面对 API 故障也是如此。通过对暂时性问题实施重试并为更持久的故障定义回退方法,Spring 应用程序可以在当今互联的数字世界中提供可靠性和弹性。
作者:Amol Gote
更多技术干货请关注公号【云原生数据库】
squids.cn,云数据库RDS,迁移工具DBMotion,云备份DBTwin等数据库生态工具。
irds.cn,多数据库管理平台(私有云)。