写点什么

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

  • 2025-07-14
    福建
  • 本文字数:3281 字

    阅读完需:约 11 分钟

前言


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

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

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


一、重试机制的原因


1.1 为什么需要重试?



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


1.2 重试的三大陷阱

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

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

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


二、基础重试方案


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")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<FailedTask> tasks = taskDao.findFailed(MAX_RETRY);    tasks.forEach(task -> {        if (retry(task)) {            task.markSuccess();        } else {            task.incrRetryCount();        }        taskDao.update(task);    });}
复制代码


关键点

  • 数据库记录失败任务

  • 低峰期批量处理

  • 独立线程池隔离资源


4.3 两阶段提交(王者Ⅰ)


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

@Transactionalpublic 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);}
复制代码


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


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 必须实现的三大防护


6.2 经典踩坑案例


  1. 坑 1:无限制重试→ 某系统因未设重试上限,线程池爆满导致集群雪崩解法maxAttempts=3 + 熔断降级

  2. 坑 2:忽略错误类型→ 参数错误(4xx)被反复重试,放大无效流量解法retryOnException(e -> e instanceof TimeoutException)

  3. 坑 3:上下文丢失→ 异步重试后丢失用户会话信息解法:重试前快照关键上下文(如 userId、requestId)


七、方案选型参考图



总结


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

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

  3. 分层防御体系:代码层:幂等性 + 超时控制框架层:退避策略 + 熔断降级架构层:异步解耦 + 持久化补偿

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


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


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


文章转载自:苏三说技术

原文链接:https://www.cnblogs.com/12lisu/p/18982777

体验地址:http://www.jnpfsoft.com/?from=001YH

用户头像

还未添加个人签名 2025-04-01 加入

还未添加个人简介

评论

发布
暂无评论
瞧瞧别人家的接口重试,那叫一个优雅!_前端_电子尖叫食人鱼_InfoQ写作社区