外卖霸王餐核销回调接口的幂等ID生成与重复请求过滤设计
在对接第三方平台(如美团、饿了么)的“霸王餐”核销回调时,由于网络抖动或重试机制,同一笔核销事件可能被多次推送。若系统未做幂等处理,将导致用户多次获得奖励、财务对账异常等严重问题。本文基于baodanbao.com.cn.*包结构,详细阐述如何通过幂等ID生成与Redis原子校验实现高可靠、低延迟的重复请求过滤。
1. 幂等ID的来源与规范
理想情况下,第三方回调应携带唯一事件ID(如event_id或request_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开发者团队,转载请注明出处!
4785

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



