Java中API无响应时,如何延迟下一次异步API调用?
你的方案完全可行,而且是处理这类无响应API调用限流与重试场景的非常合理的思路。核心是通过记录每个API的错误次数和下次允许执行的时间,避免无意义地重复创建线程去调用那些暂时不可用的API,既能节省资源,又能给故障API留出恢复时间。
下面结合你的思路给出具体的实现细节和代码修改建议:
1. 定义存储API重试信息的实体类
我们需要一个类来封装每个API的错误次数和下次执行时间,方便后续维护:
import java.time.OffsetDateTime; public class ApiRetryInfo { private int errorCount; private OffsetDateTime nextExecutionTime; // 构造方法、getter、setter public ApiRetryInfo() { this.errorCount = 0; this.nextExecutionTime = OffsetDateTime.now(); // 默认立即执行 } // 省略getter和setter方法 }
2. 用线程安全容器维护API重试状态
因为ExecutorService是多线程环境,普通HashMap会存在线程安全问题,所以必须用ConcurrentHashMap来存储每个API的重试信息(可以作为类的全局成员变量):
import java.util.concurrent.ConcurrentHashMap; private final ConcurrentHashMap<String, ApiRetryInfo> apiRetryMap = new ConcurrentHashMap<>();
3. 修改ConnectToApiCallable,在调用失败时更新重试状态
在call()方法中捕获API调用的异常,根据错误次数计算下次延迟时间并更新状态;如果调用成功,则重置该API的重试记录:
import org.springframework.web.client.ResourceAccessException; import java.time.OffsetDateTime; public class ConnectToApiCallable implements Callable<ReturnedStringAndDate> { private final String url; private final String apiName; private final ConcurrentHashMap<String, ApiRetryInfo> apiRetryMap; // 构造方法注入必要参数 public ConnectToApiCallable(String url, String apiName, ConcurrentHashMap<String, ApiRetryInfo> apiRetryMap) { this.url = url; this.apiName = apiName; this.apiRetryMap = apiRetryMap; } @Override public ReturnedStringAndDate call() throws Exception { ReturnedStringAndDate stringAndDate = new ReturnedStringAndDate(); HttpComponentsClientHttpRequestFactory clientHttpRequestFactory = new HttpComponentsClientHttpRequestFactory(HttpClientBuilder.create().build()); clientHttpRequestFactory.setConnectTimeout(4000); clientHttpRequestFactory.setReadTimeout(6000); // 建议添加读取超时,避免线程长时间阻塞 RestTemplate restTemplate = new RestTemplate(clientHttpRequestFactory); try { String json = restTemplate.getForObject(url, String.class); stringAndDate.setJsonString(json); // 调用成功,重置重试状态 ApiRetryInfo retryInfo = apiRetryMap.getOrDefault(apiName, new ApiRetryInfo()); retryInfo.setErrorCount(0); retryInfo.setNextExecutionTime(OffsetDateTime.now()); apiRetryMap.put(apiName, retryInfo); return stringAndDate; } catch (ResourceAccessException e) { // 捕获网络类异常(连接超时、无响应等) // 调用失败,更新重试状态 ApiRetryInfo retryInfo = apiRetryMap.getOrDefault(apiName, new ApiRetryInfo()); int currentErrorCount = retryInfo.getErrorCount() + 1; retryInfo.setErrorCount(currentErrorCount); // 根据错误次数计算下次执行延迟 retryInfo.setNextExecutionTime(calculateNextExecutionTime(currentErrorCount)); apiRetryMap.put(apiName, retryInfo); throw e; // 抛出异常,让上层Future处理 } } // 根据错误次数计算渐进式延迟 private OffsetDateTime calculateNextExecutionTime(int errorCount) { int delayMinutes; switch (errorCount) { case 1: delayMinutes = 1; break; case 2: delayMinutes = 2; break; case 3: delayMinutes = 5; break; default: delayMinutes = 30; // 超过3次错误后,固定延迟30分钟 break; } return OffsetDateTime.now().plusMinutes(delayMinutes); } }
4. 修改createListOfCallableTasks方法,过滤未到执行时间的API
遍历API列表时,先检查该API的下次执行时间是否已到,只有符合条件的才添加到任务列表:
public List<Callable<ReturnedStringAndDate>> createListOfCallableTasks(PoolList poolList){ List<Callable<ReturnedStringAndDate>> callableList = new ArrayList<>(); OffsetDateTime now = OffsetDateTime.now(); for (PoolItem poolItem : poolList.getPoolList()) { // 假设poolList元素类型为PoolItem String apiName = poolItem.getName(); ApiRetryInfo retryInfo = apiRetryMap.getOrDefault(apiName, new ApiRetryInfo()); // 判断是否到了允许执行的时间 if (retryInfo.getNextExecutionTime().isBefore(now) || retryInfo.getNextExecutionTime().isEqual(now)) { String url = this.appendPoolApiUrl(poolItem); callableList.add(new ConnectToApiCallable(url, apiName, apiRetryMap)); } // 未到时间则跳过,不创建任务 } return callableList; }
额外注意事项
- 线程池配置:确保你的
executorService是有限大小的(比如Executors.newFixedThreadPool(n)或自定义ThreadPoolExecutor),避免任务过多导致线程资源耗尽。 - 持久化重试状态:如果应用重启后需要保留重试信息,可以把
apiRetryMap的内容持久化到Redis或数据库中,启动时再加载回来。 - 异常范围:除了
ResourceAccessException,还可以根据实际情况捕获其他API调用相关异常(比如HttpClientErrorException),针对不同异常类型调整重试策略。
你的核心思路完全没问题,通过上述修改就能实现“避免为无响应API无限创建线程”的目标,同时实现渐进式延迟重试的效果。
内容的提问来源于stack exchange,提问作者Maciaz
相关产品推荐
相关产品推荐

