前言
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<Boolean> supplier = () -> paymentService.pay();
Supplier<Boolean> decorated = Decorators.ofSupplier(supplier)
    .withRetry(Retry.of("payment", retryConfig))
    .withCircuitBreaker(CircuitBreaker.of("payment", cbConfig))
    .decorate();
效果:某支付系统接入后超时率下降60%,熔断触发率降低90%
3.2 Guava-Retrying(钻石)
灵活定制复杂重试逻辑:
Retryer<Boolean> retryer = RetryerBuilder.<Boolean>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")
publicclass 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<FailedTask> 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<TransferRecord> pendings = recordDao.findPending(30);
    pendings.forEach(this::retryTransfer);
}
核心思想:操作前先留痕,任何失败可追溯
最近建了一些工作内推群,各大城市都有,欢迎各位HR和找工作的小伙伴进群交流,群里目前已经收集了不少的工作内推岗位。加苏三的微信:li_su223,备注:掘金+所在城市,即可进群。
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<String> 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种方案,你将拥有让系统“起死回生”的魔法!
——转载自:苏三说技术
