服务器程序中如何设计retry功能

本文探讨了客户端向服务器请求数据时如何优化超时重试机制。针对网络阻塞或服务器繁忙导致请求超时的问题,提出了将总超时时间按比例分配给各服务器的解决方案。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

集群中有client、server1, server2三台机器,client需要向server请求数据,如果server1响应超时,则请求server2。server1、server2互为备份,包含同样的数据。

client:

      for (id = 0; id < 2; id++)
      {
           if (TIMEOUT == do_request(server[id], timeout, req) )
           {
                  continue; // retry next server
           }
      }

timeout是外部传给client的总超时数。上面的代码有一个问题:当网络阻塞或者server十分繁忙的时候,do_request会超时,一旦超时,则总超时时间都被耗尽,根本没有剩余时间去重试下一个server。

因此,正确的retry设计需要考虑到“网络阻塞或者server十分繁忙”的情况,分给每个server的timeout时间只能是总timeout的一部分,代码改写如下:

      for (id = 0; id < 2; id++)
      {
           if (TIMEOUT == do_request(server[id], timeout * get_timeout_percent(), req) )
           {
                  continue; // retry next server
           }
      }

不过,对于server挂掉的情况(socket层面无法连接)do_request请求这个server会立即失败,可以不设置timeout_percent。


<think>我们需要设计一个Java程序,用于将钉钉的排班信息同步到公司内部软件中。根据之前的对话,我们已经有一些设计思路,现在需要更详细的设计方案,并参考提供的引用内容。设计要点:1.系统架构分层:数据接入层、业务逻辑层、数据持久化层。2.数据同步机制:支持全量和增量同步。3.接口集成:使用钉钉开放API,通过OAuth2.0获取token,然后调用排班API。4.异常处理:重试机制、日志记录、告警通知。5.定时任务:使用Quartz调度框架。参考引用内容:-引用[1]提到O2OA开源平台,其流程引擎支持定时任务、服务调用等,我们可以借鉴其架构设计。-引用[2]提到了接口测试和数据梳理,我们需要根据钉钉API文档测试接口并梳理数据。-引用[3]提到考勤系统设计,其中包含排班规则设定、定时任务(同步工作日、定时计算考勤)等,这些功能与我们的同步任务相关。具体步骤:1.**数据接入层**:-调用钉钉API获取排班信息。钉钉排班API可能包括:获取排班结果列表(/attendance/listSchedule)、获取排班详情等。-使用HTTP客户端(如ApacheHttpClient或SpringRestTemplate)发起请求。2.**业务逻辑层**:-数据解析:将JSON响应转换为Java对象(可使用Fastjson或Jackson)。-数据转换:将钉钉排班数据结构转换为公司内部软件的数据结构。-数据校验:检查数据的完整性和一致性(例如,检查必填字段,日期格式等)。3.**数据持久化层**:-将转换后的数据保存到公司内部软件数据库。根据引用[1]中的O2OA平台,它支持多种数据库,因此我们设计时应考虑数据库兼容性(使用SpringDataJPA或MyBatis等持久层框架,便于切换数据库)。4.**增量同步机制**:-记录上一次同步的时间戳(或使用钉钉返回的next_token),只同步该时间之后变更的数据。-在业务逻辑层中比较本地数据库已有数据,仅插入新数据或更新变更数据。5.**定时任务**:-使用Quartz或SpringTask定时触发同步任务。-定时任务配置为每天凌晨执行,也可以根据需求调整频率。6.**异常处理**:-使用重试机制(如SpringRetry)处理网络异常。-使用日志框架(如Log4j或Slf4j)记录同步过程。-同步失败时发送告警(通过邮件或集成公司内部的告警系统)。7.**监控与界面**:-提供简单的监控界面(可选),显示最近同步的时间、状态等(可参考引用[1]中的流程管理,设计一个简单的流程来跟踪同步任务状态)。代码结构示例:-`DingTalkApiClient`:封装钉钉API调用,包括获取token和排班信息。-`ScheduleSyncService`:业务逻辑,包括数据转换、校验和保存。-`ScheduleRepository`:数据持久化接口。-`ScheduleSyncJob`:定时任务,调用`ScheduleSyncService`完成同步。详细代码示例:1.钉钉API客户端:```javapublicclassDingTalkApiClient{privatestaticfinalStringGET_TOKEN_URL="https://oapi.dingtalk.com/gettoken";privatestaticfinalStringLIST_SCHEDULE_URL="https://oapi.dingtalk.com/topapi/attendance/listschedule";publicStringgetAccessToken(StringcorpId,StringcorpSecret){//使用RestTemplate请求RestTemplaterestTemplate=newRestTemplate();Stringurl=String.format("%s?appkey=%s&appsecret=%s",GET_TOKEN_URL,corpId,corpSecret);ResponseEntity<JSONObject>response=restTemplate.getForEntity(url,JSONObject.class);//检查响应并返回access_tokenreturnresponse.getBody().getString("access_token");}publicList<DingTalkSchedule>listSchedule(StringaccessToken,DateworkDate,longoffset,longsize){Stringurl=LIST_SCHEDULE_URL+"?access_token="+accessToken;JSONObjectparam=newJSONObject();param.put("workDate",DateFormatUtils.format(workDate,"yyyy-MM-dd"));//工作日期param.put("offset",offset);param.put("size",size);//发送POST请求RestTemplaterestTemplate=newRestTemplate();HttpHeadersheaders=newHttpHeaders();headers.setContentType(MediaType.APPLICATION_JSON);HttpEntity<String>request=newHttpEntity<>(param.toJSONString(),headers);ResponseEntity<JSONObject>response=restTemplate.postForEntity(url,request,JSONObject.class);//解析返回的排班列表JSONObjectbody=response.getBody();if(body.getInteger("errcode")==0){JSONObjectresult=body.getJSONObject("result");JSONArrayschedules=result.getJSONArray("schedules");returnschedules.toJavaList(DingTalkSchedule.class);}else{thrownewRuntimeException("调用钉钉排班接口失败:"+body.getString("errmsg"));}}}```2.业务逻辑层:```java@ServicepublicclassScheduleSyncService{@AutowiredprivateDingTalkApiClientdingTalkApiClient;@AutowiredprivateScheduleRepositoryscheduleRepository;@AutowiredprivateRetryTemplateretryTemplate;//用于重试//同步一天的排班数据publicvoidsyncSchedules(DateworkDate){//使用重试机制调用钉钉APIList<DingTalkSchedule>dingTalkSchedules=retryTemplate.execute(context->dingTalkApiClient.listSchedule(workDate,0,100)//假设分页,每页100条);//转换数据List<InternalSchedule>internalSchedules=convert(dingTalkSchedules);//保存到数据库scheduleRepository.saveAll(internalSchedules);}privateList<InternalSchedule>convert(List<DingTalkSchedule>dingTalkSchedules){//转换逻辑,将钉钉排班对象转换为内部排班对象returndingTalkSchedules.stream().map(d->{InternalSchedules=newInternalSchedule();//属性映射,例如:s.setUserId(d.getUserId());s.setWorkDate(d.getWorkDate());s.setShiftId(d.getShiftId());//...其他字段returns;}).collect(Collectors.toList());}}```3.定时任务:```java@Configuration@EnableSchedulingpublicclassSyncScheduleTask{@AutowiredprivateScheduleSyncServicescheduleSyncService;//每天凌晨1点执行@Scheduled(cron="001**?")publicvoidsync(){//同步昨天的排班(具体日期可根据业务调整)DateworkDate=DateUtils.addDays(newDate(),-1);scheduleSyncService.syncSchedules(workDate);}}```8.**分页处理**:钉钉API可能返回大量数据,因此需要分页获取。在`DingTalkApiClient`的`listSchedule`方法中,我们使用了`offset`和`size`参数,然后需要循环获取直到没有数据。9.**数据一致性**:在同步过程中,可能遇到部分失败的情况。为了保证数据一致性,我们可以使用数据库事务(在业务方法上添加`@Transactional`)来确保一次同步的数据全部成功保存或全部回滚。10.**性能优化**:-多线程同步:将一天的数据分成多个分页,使用多线程并行获取(需注意钉钉API的调用频率限制)。-批量保存:使用`JpaRepository`的`saveAll`方法,减少数据库交互次数。11.**非关系型数据库调整**:如果公司内部软件使用非关系型数据库(如MongoDB),则调整数据持久化层:-创建MongoRepository接口。-修改转换逻辑,将钉钉数据转换为符合MongoDB结构的文档。-批量写入使用MongoTemplate的批量操作。12.**内存溢出处理**:-对于大量数据,避免一次性加载到内存,采用分页同步,每页处理完后立即清空。-使用流式处理:逐条转换并保存。13.**断点续传**:-记录每次同步的offset,存储在数据库或文件中。当任务中断重启时,从断点处继续同步。-在增量同步中,记录上一次同步的最大ID或时间戳。以上是一个详细的设计方案,参考了提供的引用内容,特别是O2OA平台的多数据库支持和考勤系统的定时任务设计。具体实现细节还需根据钉钉API文档和公司内部软件数据库结构进行调整。</think> ### Java 钉钉排班信息同步功能设计方案 #### 1. 系统架构设计 采用分层架构,各层职责明确: | 层级 | 功能 | 实现要点 | |------|------|----------| | **数据接入层** | 钉钉API对接 | 使用Spring WebClient实现异步HTTP请求<br>OAuth2.0认证令牌管理[^2] | | **数据转换层** | 数据格式转换 | 钉钉JSON→内部DTO对象转换<br>使用MapStruct实现对象映射 | | **业务逻辑层** | 核心同步逻辑 | 增量/全量同步策略<br>排班规则校验[^3] | | **数据持久层** | 数据存储 | JPA实现多数据库支持[^1](Oracle/MySQL/MongoDB) | | **任务调度层** | 定时触发 | Quartz调度框架<br>断点续传支持 | ```mermaid graph TD A[钉钉服务器] -->|HTTP API| B(数据接入层) B --> C{数据转换层} C -->|成功| D[业务逻辑层] C -->|失败| E[异常处理] D --> F[数据持久层] G[任务调度层] --> B F --> H[(公司数据库)] ``` #### 2. 核心同步流程 1. **令牌获取** 使用企业凭证获取访问令牌: $$\text{Token} = f(\text{CorpID}, \text{CorpSecret})$$ ```java public String refreshToken() { String url = "https://oapi.dingtalk.com/gettoken?corpid={id}&corpsecret={secret}"; return webClient.get() .uri(url, corpId, corpSecret) .retrieve() .bodyToMono(TokenResponse.class) .map(TokenResponse::getToken); } ``` 2. **分页数据获取** 采用分段请求防止OOM: ```java public Flux<ScheduleData> fetchSchedules(LocalDate date) { return Flux.generate( () -> 0L, // 初始offset (offset, sink) -> { List<ScheduleData> batch = dingTalkApi.getSchedules(date, offset, BATCH_SIZE); if (batch.isEmpty()) sink.complete(); batch.forEach(sink::next); return offset + batch.size(); }); } ``` 3. **数据转换引擎** 使用注解驱动映射: ```java @Mapper public interface ScheduleMapper { @Mapping(source = "plan.checkTime", target = "shiftTime") @Mapping(source = "user.name", target = "staffName") InternalSchedule toInternal(DingTalkSchedule source); } ``` 4. **事务性存储** ```java @Transactional public void saveBatch(List<InternalSchedule> schedules) { schedules.forEach(item -> { if (!repository.existsByUniqueKey(item.getKey())) { repository.save(item); } }); } ``` #### 3. 异常处理机制 ```mermaid graph LR A[同步失败] --> B{错误类型} B -->|网络异常| C[重试3次] B -->|数据格式错误| D[记录异常数据] B -->|系统级错误| E[通知管理员] C -->|仍失败| D D --> F[异常队列] F --> G[人工处理界面] ``` #### 4. 性能优化策略 1. **二级缓存** ```java @Cacheable("dingtalkDeptCache") public Department getDept(String deptId) { return dingTalkApi.getDept(deptId); } ``` 2. **批量操作** ```sql INSERT INTO schedules (...) VALUES (...), (...), (...) ON CONFLICT DO NOTHING ``` 3. **流式处理** ```java fetchSchedules(date) .map(scheduleMapper::toInternal) .buffer(100) // 100条批处理 .subscribe(this::saveBatch); ``` #### 5. 不同数据库适配 | 数据库类型 | 持久化策略 | 特点 | |------------|-------------|------| | **关系型** | JPA+批量插入 | 使用UPSERT避免重复 | | **MongoDB** | BulkWrite | 利用$setOnInsert | | **Redis** | 管道操作 | 减少网络往返 | ```java public void saveToMongo(List<InternalSchedule> list) { List<UpdateOneModel<Document>> bulkOps = list.stream() .map(item -> new UpdateOneModel<>( Filters.eq("_id", item.getKey()), new Document("$setOnInsert", toDocument(item)), UPDATE_OPTIONS)) .collect(Collectors.toList()); mongoTemplate.bulkOps(BulkMode.ORDERED, SCHEDULE_COLLECTION) .upsert(bulkOps) .execute(); } ``` --- ### 关键技术组件 | 组件 | 功能 | 推荐实现 | |------|------|----------| | HTTP客户端 | 钉钉API调用 | Spring WebClient | | JSON处理 | 数据序列化 | Jackson | | 对象映射 | DTO转换 | MapStruct | | 任务调度 | 定时触发 | Quartz | | 数据持久化 | 多DB支持 | Spring Data JPA/MongoDB | | 监控 | 同步状态 | Micrometer+Prometheus | ### 数据同步性能指标 $$\text{吞吐量} = \frac{\text{处理数据量}}{\text{时间窗口}} \times \text{并行度}$$ 建议监控: 1. 单批次处理时间:$$t_{\text{batch}} = t_{\text{fetch}} + t_{\text{map}} + t_{\text{save}}$$ 2. 内存使用率:不超过JVM堆的70% 3. API调用成功率:≥99.5% ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值