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

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

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

幂等性机制设计

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

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

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);
}
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
}

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

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 或自定义重试逻辑均可。以下为自定义实现:

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:

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
    }
}

回调处理中的幂等性

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值