在分布式系统开发中,调用第三方API服务是家常便饭,但网络抖动、服务暂时不可用等"瞬时故障"却让人头疼。作为Java开发者,我们该如何优雅地处理这类问题?Spring-Retry框架给出了漂亮的解决方案。
去年我们团队接入了一个第三方支付接口,上线首日就遭遇了连续调用失败。查看日志发现,这些失败大多发生在网络波动期间,且重试后往往能成功。这让我深刻认识到:没有重试机制的API调用是不完整的。
典型的需要重试的场景包括:
重试的核心逻辑其实很简单:捕获异常 → 等待间隔 → 再次尝试。但自己实现一套健壮的重试机制需要考虑很多细节:
java复制// 伪代码:手动实现重试
int maxAttempts = 3;
for (int i=0; i<maxAttempts; i++) {
try {
return callAPI();
} catch (Exception e) {
if (i == maxAttempts-1) throw e;
Thread.sleep(1000 * (i+1)); // 简单线性等待
}
}
手动实现的缺点很明显:
这正是Spring-Retry框架要解决的问题。
首先引入必要依赖(Gradle示例):
groovy复制implementation 'org.springframework.retry:spring-retry:1.3.3'
implementation 'org.springframework:spring-aspects:5.3.9'
然后在启动类添加注解开启功能:
java复制@SpringBootApplication
@EnableRetry // 关键注解
public class PaymentApplication {
public static void main(String[] args) {
SpringApplication.run(PaymentApplication.class, args);
}
}
@Retryable 是核心注解,来看一个支付接口调用的实战例子:
java复制@Service
public class PaymentService {
@Retryable(
value = {PaymentException.class, SocketTimeoutException.class}, // 可重试的异常类型
maxAttempts = 4, // 最大尝试次数(含首次)
backoff = @Backoff(
delay = 1000, // 初始延迟1秒
multiplier = 2, // 延迟倍数
maxDelay = 5000 // 最大延迟5秒
)
)
public PaymentResult callPaymentAPI(PaymentRequest request) {
// 调用第三方支付接口的实现
return thirdPartyPaymentClient.process(request);
}
}
这样配置后,当支付接口调用抛出PaymentException或SocketTimeoutException时,系统会按照以下节奏重试:
| 尝试次数 | 延迟时间(ms) | 说明 |
|---|---|---|
| 1 | 0 | 第一次立即执行 |
| 2 | 1000 | 首次重试等待1秒 |
| 3 | 2000 | 第二次重试等待2秒 |
| 4 | 4000 | 第三次重试等待4秒 |
关键参数解析:
maxAttemptsExpression: 支持SpEL表达式动态设置重试次数exceptionExpression: 更精细的异常过滤条件listeners: 可以添加自定义重试监听器当所有重试都失败后,我们需要一个优雅的降级方案:
java复制@Recover
public PaymentResult fallbackPayment(PaymentException e, PaymentRequest request) {
log.warn("支付接口重试全部失败,启用降级方案", e);
// 可能的降级操作:
// 1. 返回兜底数据
// 2. 记录失败订单后续人工处理
// 3. 走备用支付通道
return PaymentResult.fail("系统繁忙,请稍后重试");
}
注意:@Recover方法必须满足:
对于特别不稳定的服务,可以启用熔断模式:
java复制@CircuitBreaker(
openTimeout = 5000, // 熔断打开的超时时间(ms)
resetTimeout = 30000, // 重置超时时间(ms)
value = PaymentException.class
)
public PaymentResult unstableService() {
// 高风险的第三方服务调用
}
熔断器的工作流程:
除了注解方式,Spring-Retry还提供了灵活的编程式API:
java复制@Configuration
public class RetryConfig {
@Bean
public RetryTemplate paymentRetryTemplate() {
RetryTemplate template = new RetryTemplate();
// 重试策略:最多5次
SimpleRetryPolicy retryPolicy = new SimpleRetryPolicy();
retryPolicy.setMaxAttempts(5);
// 退避策略:指数退避
ExponentialBackOffPolicy backOffPolicy = new ExponentialBackOffPolicy();
backOffPolicy.setInitialInterval(1000);
backOffPolicy.setMultiplier(2.0);
backOffPolicy.setMaxInterval(10000);
template.setRetryPolicy(retryPolicy);
template.setBackOffPolicy(backOffPolicy);
// 添加监听器
template.registerListener(new PaymentRetryListener());
return template;
}
}
使用RetryTemplate的代码示例:
java复制public PaymentResult processWithTemplate(PaymentRequest request) {
return paymentRetryTemplate.execute(context -> {
// 业务逻辑
return paymentGateway.process(request);
}, context -> {
// 降级逻辑
return PaymentResult.fail("支付服务暂时不可用");
});
}
通过实现RetryListener接口,我们可以监控每次重试:
java复制public class PaymentRetryListener implements RetryListener {
@Override
public <T, E extends Throwable> boolean open(RetryContext context,
RetryCallback<T, E> callback) {
// 重试开始前调用
log.info("开始重试操作: {}", context.getAttribute(RetryContext.NAME));
return true; // 返回false可以终止重试
}
@Override
public <T, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
// 每次重试失败后调用
log.warn("第{}次重试失败: {}", context.getRetryCount(),
throwable.getMessage());
}
@Override
public <T, E extends Throwable> void close(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
// 所有重试结束后调用
if (throwable != null) {
log.error("重试操作最终失败", throwable);
} else {
log.info("重试操作成功完成");
}
}
}
根据不同的业务场景,推荐以下配置组合:
| 场景类型 | maxAttempts | 退避策略 | 适用案例 |
|---|---|---|---|
| 快速失败型 | 1-2 | 无延迟或固定短延迟(300ms) | 用户界面交互请求 |
| 平衡型 | 3-5 | 指数退避(初始1s) | 支付/订单核心流程 |
| 高容忍型 | 5-10 | 随机退避(1-5s) | 后台报表生成 |
重要提示:对于写操作要特别小心,确保接口的幂等性,避免重试导致重复执行。
不加控制的重试可能导致级联故障。防护措施包括:
java复制// 示例:结合熔断器
@CircuitBreaker(maxAttempts = 3)
@Retryable(maxAttempts = 2)
public Result callUnstableService() {
// ...
}
良好的监控能帮助我们及时发现问题:
java复制@Retryable(listeners = "metricsRetryListener")
public void monitoredOperation() {
// ...
}
@Component
public class MetricsRetryListener extends RetryListenerSupport {
private final MeterRegistry meterRegistry;
@Override
public <T, E extends Throwable> void onError(RetryContext context,
RetryCallback<T, E> callback, Throwable throwable) {
meterRegistry.counter("retry.attempt",
"exception", throwable.getClass().getSimpleName())
.increment();
}
}
推荐监控指标:
问题1:@Recover方法没有被调用
问题2:重试没有生效
问题3:重试导致性能下降
java复制// 异步重试示例
@Async
@Retryable
public Future<Result> asyncOperation() {
// ...
}
Spring-Retry虽然强大,但也要根据具体业务场景合理使用。对于特别复杂的场景,可以考虑结合Resilience4j等更专业的容错库。