Java后端系统对接第三方外卖API时的幂等性设计与重试策略实践

Java后端系统对接第三方外卖API时的幂等性设计与重试策略实践

在构建高可用的外卖业务系统时,Java后端常需调用美团、饿了么等第三方外卖平台的API,如创建订单、取消订单、核销霸王餐等。由于网络抖动、服务超时或第三方限流等原因,请求可能失败或响应不确定。若盲目重试,极易导致重复下单、多次退款等严重业务问题。因此,幂等性设计合理的重试策略 成为保障系统稳定性的关键。

幂等性机制设计

幂等性指同一操作多次执行所产生的影响与一次执行相同。在外卖API场景中,可通过**唯一业务标识(BizId)**实现幂等控制。例如,在创建订单时,由本地系统生成全局唯一的 outTradeNo 作为外部订单号传给第三方平台。美团等平台会基于该字段去重,避免重复创建。

在本地系统中,也需建立幂等记录表,防止重复处理回调或重试请求:

java 复制代码
package juwatech.cn.idempotent;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface IdempotentRecordRepository extends JpaRepository<IdempotentRecord, String> {
    Optional<IdempotentRecord> findByBizId(String bizId);
}
java 复制代码
package juwatech.cn.idempotent;

import javax.persistence.Entity;
import javax.persistence.Id;
import java.time.LocalDateTime;

@Entity
public class IdempotentRecord {
    @Id
    private String bizId;
    private String actionType; // 如 "CREATE_ORDER", "CANCEL_ORDER"
    private LocalDateTime createTime;
    private String resultData; // 可存第三方返回的完整响应

    // getters and setters
}

业务逻辑中先校验幂等记录:

java 复制代码
package juwatech.cn.service;

import juwatech.cn.idempotent.IdempotentRecord;
import juwatech.cn.idempotent.IdempotentRecordRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class OrderService {

    @Autowired
    private IdempotentRecordRepository idempotentRepo;

    @Autowired
    private ThirdPartyApiClient apiClient;

    @Transactional
    public String createOrderWithIdempotency(String bizId, OrderRequest request) {
        var existing = idempotentRepo.findByBizId(bizId);
        if (existing.isPresent()) {
            return existing.get().getResultData(); // 直接返回历史结果
        }

        String response = apiClient.createOrder(request.withOutTradeNo(bizId));
        
        var record = new IdempotentRecord();
        record.setBizId(bizId);
        record.setActionType("CREATE_ORDER");
        record.setResultData(response);
        record.setCreateTime(LocalDateTime.now());
        idempotentRepo.save(record);

        return response;
    }
}

重试策略实现

对于网络异常或5xx错误,应采用指数退避+最大重试次数策略。使用 Spring Retry 或自定义重试逻辑均可。以下为自定义实现:

java 复制代码
package juwatech.cn.retry;

import java.util.concurrent.TimeUnit;
import java.util.function.Supplier;

public class RetryUtils {

    public static <T> T retry(Supplier<T> operation, int maxRetries, long baseDelayMs) {
        Exception lastException = null;
        for (int i = 0; i <= maxRetries; i++) {
            try {
                return operation.get();
            } catch (Exception e) {
                lastException = e;
                if (i < maxRetries) {
                    long delay = (long) (baseDelayMs * Math.pow(2, i));
                    try {
                        TimeUnit.MILLISECONDS.sleep(delay);
                    } catch (InterruptedException ie) {
                        Thread.currentThread().interrupt();
                        throw new RuntimeException("Retry interrupted", ie);
                    }
                }
            }
        }
        throw new RuntimeException("Operation failed after " + maxRetries + " retries", lastException);
    }
}

结合幂等ID调用第三方API:

java 复制代码
package juwatech.cn.client;

import juwatech.cn.retry.RetryUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;

import java.nio.charset.StandardCharsets;

public class ThirdPartyApiClient {

    private final CloseableHttpClient httpClient = HttpClients.createDefault();
    private static final String CREATE_ORDER_URL = "https://openapi.meituan.com/v1/order/create";

    public String createOrder(OrderRequest request) {
        return RetryUtils.retry(() -> {
            HttpPost post = new HttpPost(CREATE_ORDER_URL);
            post.setHeader("Content-Type", "application/json");
            post.setEntity(new StringEntity(request.toJson(), StandardCharsets.UTF_8));

            HttpResponse response = httpClient.execute(post);
            int status = response.getStatusLine().getStatusCode();
            String body = EntityUtils.toString(response.getEntity(), StandardCharsets.UTF_8);

            if (status >= 500) {
                throw new RuntimeException("Third-party server error: " + status);
            }
            if (status == 429) {
                throw new RuntimeException("Rate limited");
            }
            // 注意:400/401/403 等客户端错误不应重试
            return body;
        }, 3, 500); // 最多重试3次,初始延迟500ms
    }
}

回调处理中的幂等性

第三方平台异步通知(如订单状态变更回调)同样需幂等处理:

java 复制代码
package juwatech.cn.controller;

import juwatech.cn.idempotent.IdempotentRecordRepository;
import juwatech.cn.service.OrderCallbackService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MeituanCallbackController {

    @Autowired
    private IdempotentRecordRepository idempotentRepo;

    @Autowired
    private OrderCallbackService callbackService;

    @PostMapping("/callback/meituan/order")
    public String handleOrderCallback(@RequestBody String payload,
                                      @RequestHeader("X-MT-Signature") String signature,
                                      @RequestHeader("X-MT-Nonce") String nonce) {
        // 验签逻辑略
        String bizId = extractBizIdFromPayload(payload); // 从回调体提取 out_trade_no

        if (idempotentRepo.findByBizId(bizId).isPresent()) {
            return "success"; // 已处理过,直接返回成功
        }

        callbackService.processOrderUpdate(payload);
        return "success";
    }

    private String extractBizIdFromPayload(String payload) {
        // 解析 JSON 提取 bizId
        return "mock_biz_id";
    }
}

本文著作权归吃喝不愁app开发者团队,转载请注明出处!

相关推荐
写代码的【黑咖啡】2 小时前
深入理解 Python 中的模块(Module)
开发语言·python
TG:@yunlaoda360 云老大2 小时前
华为云国际站代理商的CBR主要有什么作用呢?
java·网络·华为云
wuk9982 小时前
matlab为地图进行四色着色
开发语言·matlab
_MyFavorite_2 小时前
cl报错+安装 Microsoft Visual C++ Build Tools
开发语言·c++·microsoft
charlie1145141912 小时前
现代嵌入式C++教程:C++98——从C向C++的演化(2)
c语言·开发语言·c++·学习·嵌入式·教程·现代c++
zmzb01033 小时前
C++课后习题训练记录Day55
开发语言·c++
李白同学3 小时前
C++:继承
开发语言·c++
k***92163 小时前
【C++】STL详解(九)—priority_queue的使用与模拟实现
开发语言·c++
速易达网络3 小时前
基于Java TCP 聊天室
java·开发语言·tcp/ip