Spring Boot客户端定期调用API的容错设计方案咨询
Spring Boot ClientA 定时同步账户数据最佳设计方案
一、核心定时调度与时间区间管理
- 用Spring
@Scheduled实现15分钟定时任务,配置fixedRate = 900000(15分钟毫秒值),同时加本地锁(单实例)或分布式锁(集群场景)避免任务重叠执行。 - 绝对不能硬算时间区间:在本地数据库建一张同步配置表(比如
sync_config),存last_success_end_time字段——每次任务启动时,把这个值作为本次同步的startTime,当前时间作为endTime。ClientA宕机恢复后,直接从上次成功同步的终点继续,不会漏数据也不会重复拉取旧数据。
二、API调用与容错处理
- 封装专门的API客户端类(比如
ServerAAccountClient),用RestTemplate或WebClient发起请求,设置5秒左右的连接/读取超时,避免线程阻塞。 - 容错策略:
- 重试用Resilience4j或Spring Retry,限制重试次数(比如3次),用指数退避间隔。因为ServerA的API是按时间区间返回数据,只要
startTime和endTime固定,返回结果完全一致,重试不会导致重复数据。 - 当ServerA宕机超过重试次数,把本次待同步的时间区间存入失败任务表(比如
sync_failed_task,字段包含start_time、end_time、retry_count、status),留待后续补偿。
- 重试用Resilience4j或Spring Retry,限制重试次数(比如3次),用指数退避间隔。因为ServerA的API是按时间区间返回数据,只要
三、数据持久化与幂等性保障
- 本地数据库的账户表给
account_no加唯一约束,插入时用INSERT IGNORE或者捕获唯一约束异常直接忽略——就算重复拉取到相同账户,也不会造成本地数据重复。 - 同步流程:
- 读取
sync_config的last_success_end_time作为本次startTime,当前时间为endTime。 - 调用ServerA API获取账户列表。
- 批量插入本地数据库,自动忽略重复账户。
- 插入成功后,更新
sync_config的last_success_end_time为本次endTime。 - 若API调用或插入失败,把时间区间存入
sync_failed_task标记为待重试,抛出异常触发重试(若配置了重试)。
- 读取
四、异常恢复与补偿机制
- 单独开一个每分钟执行的定时任务,扫描
sync_failed_task中待重试且重试次数未达上限的任务,重新执行同步流程。 - 补偿任务执行后:成功则更新
sync_config并删除失败任务;失败则累加重试次数,次数耗尽后标记为失败并触发告警(邮件/钉钉通知),等待人工介入。 - ClientA启动时优先扫描
sync_failed_task,先处理未完成的补偿任务,再启动正常的15分钟同步任务。
五、关键代码示例
1. 定时任务配置
@Configuration @EnableScheduling @EnableRetry public class SyncTaskConfig { @Autowired private AccountSyncService syncService; @Scheduled(fixedRate = 900000) @Retryable(value = {ApiInvokeException.class}, maxAttempts = 3, backoff = @Backoff(delay = 2000, multiplier = 2)) public void syncAccountsTask() { syncService.syncAccounts(); } }
2. 同步核心逻辑
@Service public class AccountSyncService { @Autowired private ServerAAccountClient apiClient; @Autowired private AccountRepo accountRepo; @Autowired private SyncConfigRepo configRepo; @Autowired private FailedTaskRepo failedTaskRepo; public void syncAccounts() { // 获取上次同步终点,无则初始化当前时间减15分钟 SyncConfig config = configRepo.findById("account_sync") .orElseGet(() -> { SyncConfig initConfig = new SyncConfig(); initConfig.setId("account_sync"); initConfig.setLastSuccessEndTime(LocalDateTime.now().minusMinutes(15)); return configRepo.save(initConfig); }); LocalDateTime startTime = config.getLastSuccessEndTime(); LocalDateTime endTime = LocalDateTime.now(); try { List<Long> accountNos = apiClient.fetchAccounts(startTime, endTime); // 批量插入,忽略重复 accountRepo.batchInsertIgnore(accountNos); // 更新同步终点 config.setLastSuccessEndTime(endTime); configRepo.save(config); } catch (Exception e) { // 记录失败任务 SyncFailedTask task = new SyncFailedTask(); task.setStartTime(startTime); task.setEndTime(endTime); task.setRetryCount(0); task.setStatus(TaskStatus.PENDING); failedTaskRepo.save(task); throw new ApiInvokeException("同步账户失败", e); } } }
3. API客户端实现
@Component public class ServerAAccountClient { private final RestTemplate restTemplate; private static final String API_URI = "http://somedomainname.com/getAccounts"; public ServerAAccountClient(RestTemplate restTemplate) { this.restTemplate = restTemplate; // 设置超时 SimpleClientHttpRequestFactory factory = (SimpleClientHttpRequestFactory) restTemplate.getRequestFactory(); factory.setConnectTimeout(5000); factory.setReadTimeout(5000); } public List<Long> fetchAccounts(LocalDateTime startTime, LocalDateTime endTime) { UriComponents uri = UriComponentsBuilder.fromHttpUrl(API_URI) .queryParam("startTime", startTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) .queryParam("endTime", endTime.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)) .build(); return restTemplate.getForObject(uri.toUri(), new ParameterizedTypeReference<List<Long>>() {}); } }
六、优化建议
- 日志:详细记录每次同步的时间区间、账户数量、执行状态,方便排查问题。
- 监控:用Spring Boot Actuator暴露同步指标(成功/失败次数、待补偿任务数),结合Prometheus+Grafana做可视化监控。
- 集群场景:用Redis分布式锁保证同一时间只有一个节点执行同步,同步配置和失败任务存在共享数据库中。
内容的提问来源于stack exchange,提问作者Ramden
相关产品推荐
相关产品推荐

