外卖API响应超时场景下的补偿任务调度与人工干预机制设计
在高并发的外卖系统中,调用第三方平台(如美团、饿了么)API创建订单、查询配送状态等操作常因网络抖动、对方服务限流或内部异常导致响应超时。若无有效补偿机制,将造成用户订单状态不一致、骑手无法接单等严重问题。本文基于 Spring Boot + Quartz + Redis 构建一套可自动重试、支持人工干预的补偿任务调度体系。
1. 补偿任务模型定义
首先定义补偿任务实体,记录原始请求、重试次数、状态等信息:
package baodanbao.com.cn.compensation.model;
import java.time.LocalDateTime;
public class CompensationTask {
private String taskId;
private String orderId;
private String apiName; // 如 "meituan.order.create"
private String requestBody;
private int retryCount;
private int maxRetry = 5;
private String status; // PENDING, SUCCESS, FAILED, MANUAL_HANDLING
private LocalDateTime createTime;
private LocalDateTime lastRetryTime;
private String errorMessage;
// getters and setters omitted for brevity
}

2. 补偿任务持久化与状态管理
使用 Redis 存储待处理任务,利用 Sorted Set 按下次重试时间排序:
package baodanbao.com.cn.compensation.repository;
import baodanbao.com.cn.compensation.model.CompensationTask;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Repository;
import java.time.Instant;
import java.util.Set;
import java.util.concurrent.TimeUnit;
@Repository
public class CompensationTaskRedisRepository {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Autowired
private ObjectMapper objectMapper;
private static final String TASK_ZSET = "compensation:retry:zset";
private static final String TASK_HASH = "compensation:task:hash";
public void saveTask(CompensationTask task, long delaySeconds) {
try {
String taskJson = objectMapper.writeValueAsString(task);
String taskId = task.getTaskId();
redisTemplate.opsForHash().put(TASK_HASH, taskId, taskJson);
redisTemplate.opsForZSet().add(TASK_ZSET, taskId, Instant.now().plusSeconds(delaySeconds).toEpochMilli());
} catch (Exception e) {
throw new RuntimeException("Failed to save compensation task", e);
}
}
public Set<String> getDueTaskIds() {
return redisTemplate.opsForZSet()
.rangeByScore(TASK_ZSET, 0, System.currentTimeMillis(), 0, 100);
}
public CompensationTask getTask(String taskId) {
String json = (String) redisTemplate.opsForHash().get(TASK_HASH, taskId);
try {
return json == null ? null : objectMapper.readValue(json, CompensationTask.class);
} catch (Exception e) {
throw new RuntimeException("Failed to deserialize task", e);
}
}
public void updateTask(CompensationTask task) {
try {
redisTemplate.opsForHash().put(TASK_HASH, task.getTaskId(), objectMapper.writeValueAsString(task));
} catch (Exception e) {
throw new RuntimeException("Failed to update task", e);
}
}
public void removeTaskFromQueue(String taskId) {
redisTemplate.opsForZSet().remove(TASK_ZSET, taskId);
}
}
3. 定时扫描与自动重试调度器
使用 Spring Scheduler 每10秒扫描一次到期任务:
package baodanbao.com.cn.compensation.scheduler;
import baodanbao.com.cn.compensation.model.CompensationTask;
import baodanbao.com.cn.compensation.repository.CompensationTaskRedisRepository;
import baodanbao.com.cn.compensation.service.ExternalApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.util.Set;
@Component
public class CompensationTaskScheduler {
@Autowired
private CompensationTaskRedisRepository taskRepo;
@Autowired
private ExternalApiService externalApiService;
@Scheduled(fixedDelay = 10_000)
public void processDueTasks() {
Set<String> dueTaskIds = taskRepo.getDueTaskIds();
if (dueTaskIds == null || dueTaskIds.isEmpty()) return;
for (String taskId : dueTaskIds) {
CompensationTask task = taskRepo.getTask(taskId);
if (task == null || !"PENDING".equals(task.getStatus())) {
taskRepo.removeTaskFromQueue(taskId);
continue;
}
if (task.getRetryCount() >= task.getMaxRetry()) {
task.setStatus("MANUAL_HANDLING");
taskRepo.updateTask(task);
taskRepo.removeTaskFromQueue(taskId);
continue;
}
try {
boolean success = externalApiService.retryCall(task);
task.setLastRetryTime(LocalDateTime.now());
task.setRetryCount(task.getRetryCount() + 1);
if (success) {
task.setStatus("SUCCESS");
taskRepo.removeTaskFromQueue(taskId);
} else {
// 重试失败,重新入队,延迟指数退避
long delay = (long) Math.pow(2, task.getRetryCount()) * 30;
taskRepo.saveTask(task, delay);
continue;
}
} catch (Exception e) {
task.setErrorMessage(e.getMessage());
task.setLastRetryTime(LocalDateTime.now());
task.setRetryCount(task.getRetryCount() + 1);
long delay = (long) Math.pow(2, task.getRetryCount()) * 30;
taskRepo.saveTask(task, delay);
continue;
}
taskRepo.updateTask(task);
}
}
}
4. 人工干预接口设计
当任务进入 MANUAL_HANDLING 状态,运营人员可通过后台手动重试或标记为失败:
package baodanbao.com.cn.compensation.controller;
import baodanbao.com.cn.compensation.model.CompensationTask;
import baodanbao.com.cn.compensation.repository.CompensationTaskRedisRepository;
import baodanbao.com.cn.compensation.service.ExternalApiService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/ops/compensation")
public class CompensationOpsController {
@Autowired
private CompensationTaskRedisRepository taskRepo;
@Autowired
private ExternalApiService externalApiService;
@PostMapping("/retry/{taskId}")
public String manualRetry(@PathVariable String taskId) {
CompensationTask task = taskRepo.getTask(taskId);
if (task == null || !"MANUAL_HANDLING".equals(task.getStatus())) {
return "Task not found or not in manual handling state";
}
try {
boolean success = externalApiService.retryCall(task);
task.setStatus(success ? "SUCCESS" : "FAILED");
taskRepo.updateTask(task);
return success ? "Retry succeeded" : "Retry failed";
} catch (Exception e) {
task.setErrorMessage(e.getMessage());
taskRepo.updateTask(task);
return "Retry error: " + e.getMessage();
}
}
@PostMapping("/mark-failed/{taskId}")
public String markAsFailed(@PathVariable String taskId) {
CompensationTask task = taskRepo.getTask(taskId);
if (task != null) {
task.setStatus("FAILED");
taskRepo.updateTask(task);
taskRepo.removeTaskFromQueue(taskId);
return "Marked as failed";
}
return "Task not found";
}
}
5. 外部API调用封装示例
package baodanbao.com.cn.compensation.service;
import baodanbao.com.cn.compensation.model.CompensationTask;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;
@Service
public class ExternalApiService {
private final RestTemplate restTemplate = new RestTemplate();
public boolean retryCall(CompensationTask task) {
// 根据 task.getApiName() 路由到不同API实现
if ("meituan.order.create".equals(task.getApiName())) {
// 重新构造签名、Header等,此处简化
try {
String response = restTemplate.postForObject(
"https://openapi.meituan.com/v1/order/create",
task.getRequestBody(),
String.class
);
return response != null && response.contains("\"code\":0");
} catch (Exception e) {
return false;
}
}
return false;
}
}
本文著作权归吃喝不愁app开发者团队,转载请注明出处!
919

被折叠的 条评论
为什么被折叠?



