Spring Framework RestClient如何在每次API调用间设置间隔?
问题场景
在Spring Framework中使用org.springframework.web.client.RestClient调用外部API时,遭遇API限流限制(每秒仅允许1次调用),触发报错:
org.springframework.web.client.HttpClientErrorException$TooManyRequests: too many request 2 times in 1000 millseconds
现有核心代码如下:
RestClient配置类
@Configuration public class CloudConfig { @Value("${experimental.host}") String baseURI; @Bean RestClient restClient(RestClient.Builder builder){ return builder.baseUrl(baseURI).build(); } }
API调用服务类
@Component public class RequestService { private final RestClient restClient; // 补充原代码遗漏的构造注入 public RequestService(RestClient restClient) { this.restClient = restClient; } public String callAPI(int userID){ Map<String, Object> properties = Map.of("id", userID); return restClient.post() .uri("/external") .body(properties) .retrieve() .body(String.class); } }
定时任务调用逻辑
@Service public class RabbitMQProducer { private final UserRepository repository; private final RequestService requestService; // 补充原代码遗漏的构造注入 public RabbitMQProducer(UserRepository repository, RequestService requestService) { this.repository = repository; this.requestService = requestService; } @Scheduled(fixedRate = 10000) public void sendUserData(){ for(User user : repository.findAll()) { String data = requestService.callAPI(user.getID()); // 后续处理逻辑 } } }
直接使用Thread.sleep(1000)虽能临时规避问题,但并非最优解,以下是更合理的技术方案:
最优解决方案
1. 令牌桶限流(Guava RateLimiter)
采用令牌桶算法精准控制请求速率,比固定sleep更灵活,既能保证长期速率不超限,也能处理允许范围内的突发请求。
实现步骤:
- 引入Guava依赖(Maven):
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>32.1.3-jre</version> </dependency>
- 修改
RequestService集成RateLimiter:
@Component public class RequestService { private final RestClient restClient; private final RateLimiter rateLimiter; public RequestService(RestClient restClient) { this.restClient = restClient; // 每秒生成1个令牌,对应每秒1次API调用 this.rateLimiter = RateLimiter.create(1.0); } public String callAPI(int userID){ // 阻塞等待获取令牌,自动控制请求间隔 rateLimiter.acquire(); Map<String, Object> properties = Map.of("id", userID); return restClient.post() .uri("/external") .body(properties) .retrieve() .body(String.class); } }
优势:无需手动处理线程休眠,RateLimiter自动适配请求间隔;若某次API调用耗时超过1秒,后续请求会自动调整间隔,确保整体速率合规;支持配置预热期,适配API的流量突发规则。
2. 限流重试+指数退避
即使做了前置限流,仍可能因网络延迟、API侧计数误差触发限流错误,结合重试+指数退避策略,提升系统容错性。
实现步骤:
- 引入Spring Retry依赖:
<dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>2.0.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
- 启动类添加注解启用重试:
@SpringBootApplication @EnableRetry public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }
- 修改
RequestService添加重试逻辑:
@Component @Slf4j public class RequestService { private final RestClient restClient; private final RateLimiter rateLimiter; public RequestService(RestClient restClient) { this.restClient = restClient; this.rateLimiter = RateLimiter.create(1.0); } @Retryable( value = {HttpClientErrorException.TooManyRequests.class}, backoff = @Backoff(delay = 1000, multiplier = 2) // 首次延迟1秒,后续指数递增 ) public String callAPI(int userID){ rateLimiter.acquire(); Map<String, Object> properties = Map.of("id", userID); return restClient.post() .uri("/external") .body(properties) .retrieve() .onStatus(HttpStatusCode::is4xxClientError, (request, response) -> { if (response.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) { throw new HttpClientErrorException.TooManyRequests(response.getStatusCode(), response.getStatusText()); } }) .body(String.class); } @Recover public String recoverTooManyRequests(HttpClientErrorException.TooManyRequests e, int userID) { // 最终重试失败的降级处理:记录日志、标记用户待重试等 log.error("API调用多次失败,用户ID: {}", userID, e); return null; } }
3. 异步延迟队列串行调用
若不希望定时任务线程被阻塞,可将用户任务放入延迟队列,由单线程池按固定间隔执行。
实现步骤:
- 配置单线程任务调度器:
@Configuration public class AsyncConfig { @Bean public ThreadPoolTaskScheduler taskScheduler() { ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler(); scheduler.setPoolSize(1); // 单线程保证串行执行 scheduler.setThreadNamePrefix("api-call-scheduler-"); return scheduler; } }
- 修改
RabbitMQProducer实现延迟调度:
@Service public class RabbitMQProducer { private final UserRepository repository; private final RequestService requestService; private final ThreadPoolTaskScheduler taskScheduler; public RabbitMQProducer(UserRepository repository, RequestService requestService, ThreadPoolTaskScheduler taskScheduler) { this.repository = repository; this.requestService = requestService; this.taskScheduler = taskScheduler; } @Scheduled(fixedRate = 10000) public void sendUserData(){ List<User> users = repository.findAll(); for (int i = 0; i < users.size(); i++) { User user = users.get(i); // 按顺序延迟i*1000毫秒执行,实现每隔1秒调用一次 taskScheduler.schedule(() -> { String data = requestService.callAPI(user.getID()); // 后续处理逻辑 }, System.currentTimeMillis() + i * 1000); } } }
优势:定时任务线程无需等待API调用完成,任务在后台串行执行,不影响其他定时任务逻辑。
方案对比
- 令牌桶限流:最推荐,代码侵入性低,速率控制精准,适配绝大多数场景
- 重试+退避:作为限流的补充机制,处理突发限流错误,提升系统稳定性
- 异步延迟队列:适合无需同步等待API结果的场景,避免阻塞主线程
内容的提问来源于stack exchange,提问作者Romillion
相关产品推荐
相关产品推荐

