外卖API响应超时场景下的补偿任务调度与人工干预机制设计

外卖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开发者团队,转载请注明出处!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值