外卖霸王餐核销回调接口的幂等ID生成与重复请求过滤设计

外卖霸王餐核销回调接口的幂等ID生成与重复请求过滤设计

在对接第三方平台(如美团、饿了么)的“霸王餐”核销回调时,由于网络抖动或重试机制,同一笔核销事件可能被多次推送。若系统未做幂等处理,将导致用户多次获得奖励、财务对账异常等严重问题。本文基于baodanbao.com.cn.*包结构,详细阐述如何通过幂等ID生成Redis原子校验实现高可靠、低延迟的重复请求过滤。

1. 幂等ID的来源与规范

理想情况下,第三方回调应携带唯一事件ID(如event_idrequest_id)。若无,则需根据业务关键字段组合生成。例如,美团核销回调通常包含:

{
  "order_id": "MT20240515123456",
  "activity_id": "ACT999",
  "user_id": "U8888",
  "timestamp": 1715788800
}

可构造幂等键:order_id + "_" + activity_id,因其在单次活动中唯一。
在这里插入图片描述

2. Redis原子校验实现

使用Redis的SET key EX seconds NX命令实现“仅当不存在时设置”,天然具备原子性。若设置成功,说明是首次请求;否则为重复。

package baodanbao.com.cn.service.idempotent;

import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.util.concurrent.TimeUnit;

@Service
public class IdempotentService {

    private final StringRedisTemplate redisTemplate;

    public IdempotentService(StringRedisTemplate redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    /**
     * 尝试获取幂等锁
     * @param idempotentKey 幂等ID
     * @param expireSeconds 过期时间(秒)
     * @return true 表示首次请求,false 表示重复
     */
    public boolean tryAcquire(String idempotentKey, long expireSeconds) {
        Boolean result = redisTemplate.opsForValue()
                .setIfAbsent(idempotentKey, "1", expireSeconds, TimeUnit.SECONDS);
        return Boolean.TRUE.equals(result);
    }
}

3. 回调接口实现

在Controller中提取幂等ID并校验:

package baodanbao.com.cn.controller.callback;

import baodanbao.com.cn.model.MeituanRedeemCallback;
import baodanbao.com.cn.service.idempotent.IdempotentService;
import baodanbao.com.cn.service.RedeemProcessService;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class MeituanCallbackController {

    private final IdempotentService idempotentService;
    private final RedeemProcessService redeemProcessService;

    public MeituanCallbackController(IdempotentService idempotentService,
                                     RedeemProcessService redeemProcessService) {
        this.idempotentService = idempotentService;
        this.redeemProcessService = redeemProcessService;
    }

    @PostMapping("/api/callback/meituan/redeem")
    public String handleRedeemCallback(@RequestBody MeituanRedeemCallback callback) {
        // 构造幂等ID:订单ID + 活动ID
        String idempotentKey = "redeem:idempotent:" + callback.getOrderId() + ":" + callback.getActivityId();

        // 幂等校验,过期时间设为24小时(防止极端重试)
        if (!idempotentService.tryAcquire(idempotentKey, 86400)) {
            // 已处理过,直接返回成功(避免第三方持续重试)
            return "success";
        }

        // 执行核销业务逻辑
        redeemProcessService.processRedeem(callback);

        return "success";
    }
}

4. 幂等ID生成策略封装

为统一处理不同来源回调,可抽象幂等ID生成器:

package baodanbao.com.cn.util;

public class IdempotentKeyGenerator {

    public static String generateForMeituanRedeem(String orderId, String activityId) {
        if (orderId == null || activityId == null) {
            throw new IllegalArgumentException("orderId 或 activityId 不能为空");
        }
        return "meituan:redeem:" + orderId + ":" + activityId;
    }

    public static String generateForElemeRedeem(String tradeNo, String campaignId) {
        return "eleme:redeem:" + tradeNo + ":" + campaignId;
    }
}

在Controller中调用:

String idempotentKey = IdempotentKeyGenerator.generateForMeituanRedeem(
    callback.getOrderId(), callback.getActivityId());

5. 异常场景处理

若业务逻辑执行失败(如数据库异常),不应释放幂等锁,否则下次重试会再次进入。因此,幂等锁一旦获取成功,无论业务成败,均视为“已处理”。但需记录失败日志供人工干预:

try {
    redeemProcessService.processRedeem(callback);
} catch (Exception e) {
    // 记录错误,但不释放幂等锁
    log.error("核销处理失败,幂等ID: {}", idempotentKey, e);
    // 仍返回 success,避免第三方无限重试
}

注意:此设计假设第三方回调最终一致性。若需严格重试,应引入状态机+补偿机制,但超出本文范围。

6. Redis Key 设计建议

  • 前缀清晰:redeem:idempotent:...
  • TTL合理:通常24~72小时,覆盖最大重试窗口;
  • 避免内存泄漏:确保所有路径都设置过期时间。

7. 单元测试验证

package baodanbao.com.cn.test;

import baodanbao.com.cn.service.idempotent.IdempotentService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import static org.junit.jupiter.api.Assertions.*;

@SpringBootTest
public class IdempotentServiceTest {

    @Autowired
    private IdempotentService idempotentService;

    @Test
    void testDuplicateRequestFilter() {
        String key = "test:idempotent:123";

        assertTrue(idempotentService.tryAcquire(key, 10));
        assertFalse(idempotentService.tryAcquire(key, 10)); // 第二次应失败
    }
}

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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值