瞧瞧别人家的接口重试,那叫一个优雅!

2025-10-07 11:52:07 世界杯2019

前言

2025年某电商平台深夜故障,因重试策略不当导致银行退款接口被调用82次,引发重复退款126万元!

复盘发现:80%的开发者认为重试就是for循环+Thread.sleep(),却忽略了重试风暴、幂等性缺失、资源雪崩等致命问题。

这篇文章跟大家一起聊聊接口重试的8种常用方案,希望对你会有所帮助。

一、重试机制的原因

1.1 为什么需要重试?

临时性故障占比超70%,合理重试可将成功率提升至99%以上。

1.2 重试的三大陷阱

重试风暴:固定间隔重试引发请求洪峰(如万次重试压垮服务)

数据不一致:非幂等操作导致重复生效(如重复扣款)

链路阻塞:长时重试耗尽线程资源(如数据库连接池枯竭)

二、基础重试方案

2.1 暴力轮回法(青铜)

问题代码:

// 危险!切勿直接用于生产!

public void sendSms(String phone) {

int retry = 0;

while (retry < 5) {

try {

smsClient.send(phone);

break;

} catch (Exception e) {

retry++;

Thread.sleep(1000); // 固定1秒间隔

}

}

}

事故案例:某平台短信接口重试风暴,触发第三方熔断封禁。

优化方向:增加随机抖动 + 异常过滤。

2.2 Spring Retry(黄金)

声明式注解控制重试:

@Retryable(

value = {TimeoutException.class}, // 仅重试超时异常

maxAttempts = 3,

backoff = @Backoff(delay = 1000, multiplier = 2) // 指数退避:1s→2s→4s

)

public boolean queryOrder(String orderId) {

return httpClient.get("/order/" + orderId);

}

@Recover // 兜底降级

public boolean fallback(TimeoutException e) {

return false;

}

优势:

注解驱动,业务零侵入

支持指数退避策略

无缝集成熔断器@CircuitBreaker

三、高阶重试方案

3.1 Resilience4j(白金)

应对高并发场景的重试+熔断组合拳:

// 重试配置:指数退避+随机抖动

RetryConfig retryConfig = RetryConfig.custom()

.maxAttempts(3)

.intervalFunction(IntervalFunction.ofExponentialRandomBackoff(

1000L, 2.0, 0.3 // 初始1s,指数倍率2,抖动率30%

))

.retryOnException(e -> e instanceof TimeoutException)

.build();

// 熔断配置:错误率超50%触发熔断

CircuitBreakerConfig cbConfig = CircuitBreakerConfig.custom()

.slidingWindow(10, 10, COUNT_BASED)

.failureRateThreshold(50)

.build();

// 组合装饰

Supplier supplier = () -> paymentService.pay();

Supplier decorated = Decorators.ofSupplier(supplier)

.withRetry(Retry.of("payment", retryConfig))

.withCircuitBreaker(CircuitBreaker.of("payment", cbConfig))

.decorate();

效果:某支付系统接入后超时率下降60%,熔断触发率降低90%

3.2 Guava-Retrying(钻石)

灵活定制复杂重试逻辑:

Retryer retryer = RetryerBuilder.newBuilder()

.retryIfResult(Predicates.equalTo(false)) // 返回false重试

.retryIfExceptionOfType(IOException.class)

.withWaitStrategy(WaitStrategies.exponentialWait(1000, 30, TimeUnit.SECONDS))

.withStopStrategy(StopStrategies.stopAfterAttempt(5))

.build();

retryer.call(() -> uploadService.upload(file)); // 执行

核心能力:

支持结果/异常双模式触发

提供7种等待策略(随机、指数、递增等)

可监听每次重试事件

四、分布式重试方案

4.1 MQ延时队列(星耀Ⅰ)

适用场景:异步解耦的高并发系统(如物流状态同步)

架构原理:

RocketMQ实现:

// 生产者发送延时消息

Message msg = new Message();

msg.setBody(orderData);

msg.setDelayTimeLevel(3); // RocketMQ预设10秒延迟

rocketMQTemplate.send(msg);

// 消费者

@RocketMQMessageListener(topic = "RETRY_TOPIC")

public class RetryConsumer {

public void consume(Message msg) {

try {

process(msg);

} catch (Exception e) {

// 提升延迟级别重发

msg.setDelayTimeLevel(5);

resend(msg);

}

}

}

优势:

重试与业务逻辑解耦

天然支持梯度延时

死信队列兜底人工处理

4.2 定时任务补偿(星耀Ⅱ)

适用场景:允许延迟的批处理任务(如文件导入)

@Scheduled(cron = "0 0/5 * * * ?") // 每5分钟执行

public void retryFailedTasks() {

List tasks = taskDao.findFailed(MAX_RETRY);

tasks.forEach(task -> {

if (retry(task)) {

task.markSuccess();

} else {

task.incrRetryCount();

}

taskDao.update(task);

});

}

关键点:

数据库记录失败任务

低峰期批量处理

独立线程池隔离资源

4.3 两阶段提交(王者Ⅰ)

金融级一致性保障(如转账):

@Transactional

public void transfer(TransferRequest req) {

// 阶段1:持久化操作流水

TransferRecord record = recordDao.create(req, PENDING);

// 阶段2:调用银行接口

boolean success = bankClient.transfer(req);

// 更新状态

recordDao.updateStatus(record.getId(), success ? SUCCESS : FAILED);

if (!success) {

mqTemplate.send("TRANSFER_RETRY_QUEUE", req); // 触发异步重试

}

}

// 补偿任务(扫描挂起流水)

@Scheduled(fixedRate = 30000)

public void compensate() {

List pendings = recordDao.findPending(30);

pendings.forEach(this::retryTransfer);

}

核心思想:操作前先留痕,任何失败可追溯

4.4 分布式锁重试(王者Ⅱ)

防重复提交终极方案(如秒杀):

public boolean retryWithLock(String key, int maxRetry) {

String lockKey = "RETRY_LOCK:" + key;

for (int i = 0; i < maxRetry; i++) {

if (redis.setIfAbsent(lockKey, "1", 30, SECONDS)) {

try {

return callApi(); // 持有锁时执行

} finally {

redis.delete(lockKey);

}

}

Thread.sleep(1000 * (i + 1)); // 等待锁释放

}

return false;

}

适用场景:

多实例部署环境

高竞争资源访问

等幂性要求极高业务

五、响应式重试:Spring WebFlux方案

5.1 响应式重试操作符

Mono remoteCall = Mono.fromCallable(() -> {

if (Math.random() > 0.5) throw new RuntimeException("模拟失败");

return "Success";

});

remoteCall.retryWhen(Retry.backoff(3, Duration.ofSeconds(1))

.doBeforeRetry(signal -> log.warn("第{}次重试", signal.totalRetries()))

.subscribe();

策略支持:

指数退避:Retry.backoff(maxAttempts, firstBackoff)

随机抖动:.jitter(0.5)

条件过滤:.filter(ex -> ex instanceof TimeoutException)

六、重试的避坑指南

6.1 必须实现的三大防护

防护类型

目的

实现方案

幂等性防护

防止重复生效

唯一ID+状态机

重试风暴防护

避免洪峰冲击

指数退避+随机抖动

资源隔离

保护主链路资源

线程池隔离/熔断器

6.2 经典踩坑案例

坑1:无限制重试

→ 某系统因未设重试上限,线程池爆满导致集群雪崩

解法:maxAttempts=3 + 熔断降级

坑2:忽略错误类型

→ 参数错误(4xx)被反复重试,放大无效流量

解法:retryOnException(e -> e instanceof TimeoutException)

坑3:上下文丢失

→ 异步重试后丢失用户会话信息

解法:重试前快照关键上下文(如userId、requestId)

七、方案选型参考图

总结

敬畏每一次重试:重试不是暴力补救,而是精密流量控制。

面向失败设计:假设网络不可靠、服务会宕机、资源终将枯竭。

分层防御体系:

代码层:幂等性 + 超时控制

框架层:退避策略 + 熔断降级

架构层:异步解耦 + 持久化补偿

没有银弹:秒杀场景用分布式锁,支付系统用两阶段提交,IoT设备用MQTT重试机制。

正如分布式系统大师Leslie Lamport所言:“重试是分布式系统的成人礼”。

掌握这8种方案,你将拥有让系统“起死回生”的魔法!

最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。

求一键三连:点赞、转发、在看。

关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。

本文收录于我的技术网站:http://www.susan.net.cn