接口幂等与防重提交实现

用户双击提交、网络超时重试、消息队列重投、支付回调重放——同一请求可能被执行多次。幂等指"重复执行与执行一次结果一致",是金融、下单、支付类接口的硬要求。实现手段分层:前端防"手滑",后端防"重放"。

场景与思路

  • 表单/按钮防重:短时间内同一用户重复提交;
  • 网络重试:客户端超时后重发同一请求;
  • 消息重投:MQ at-least-once 语义下的重复消费;
  • 回调重放:第三方(如支付)可能重发通知。

核心一句话:为请求找到一个唯一键,重复的键只处理一次

方案一:令牌防重(针对表单/按钮)

进入页面时向服务端申请一次性令牌,服务端存 Redis;提交时原子消费,消费成功才执行业务:

// 1) 生成令牌接口:token = UUID,setIfAbsent 存入 Redis 并设过期
String token = UUID.randomUUID().toString();
redisTemplate.opsForValue().setIfAbsent("idem:token:" + token, "1", Duration.ofMinutes(10));

// 2) 提交时原子"检查并删除",防止并发下双双通过
String lua = "if redis.call('get', KEYS[1]) == ARGV[1] then "
           + "return redis.call('del', KEYS[1]) else return 0 end";
Long consumed = redisTemplate.execute(
        new DefaultRedisScript<>(lua, Long.class),
        List.of("idem:token:" + token), "1");
if (consumed == null || consumed == 0) {
    throw new BizException("请勿重复提交");   // 输出:令牌已被消费
}

用 Lua 保证"检查+删除"原子性;也可用第 34 章 Redisson 的锁做同语义控制。

方案二:业务幂等键 + Redis/DB 去重(针对接口重放)

调用方传入业务唯一键(订单号、支付流水号、或客户端生成的 requestId),服务端先查重再处理:

// 支付回调示例:以商户订单号为幂等键
String key = "idem:pay:" + outTradeNo;
Boolean first = redisTemplate.opsForValue()
        .setIfAbsent(key, "PROCESSING", Duration.ofMinutes(30));
if (Boolean.FALSE.equals(first)) {
    return;                       // 输出:重复回调,直接返回成功(已处理过)
}
try {
    doPaySuccess(outTradeNo);     // 真正业务:更新订单状态
    redisTemplate.opsForValue().set(key, "DONE", Duration.ofMinutes(30));
} catch (Exception e) {
    redisTemplate.delete(key);    // 失败要清除标记,允许重试
    throw e;
}

更硬的兜底是数据库唯一约束:给业务键建唯一索引,重复插入抛 DuplicateKeyException 时捕获并返回"已处理"。Redis 标记 + DB 唯一索引双保险最常见。

方案三:状态机推进

适合有明确状态的业务(订单:待支付 → 已支付 → 已退款)。只允许单向流转,重复的"已支付"回调因状态不匹配被直接拒绝——天然幂等,且是方案二的补充校验。

MQ 消费者幂等

第 36/37 章说过重复消费是常态:消费者以消息里的业务键(订单号/msgId)查去重表或 Redis 去重;"记录去重标记"要与业务处理放同一事务,否则标记先写成功、业务后失败会漏处理。

落地要点

  • 幂等键唯一性由调用方保证:服务端要校验必填、长度、格式,必要时用全局 ID 生成器;
  • 幂等窗口 ≥ 最大重试时间窗,过期要能重新受理;
  • 重复请求返回什么:直接返回成功(或上次结果),别让调用方误判失败再重试;
  • 高频提交场景可与第 34 章分布式锁组合:锁防并发,幂等键防重放。

小结:防重分三层——前端令牌防手滑、服务端幂等键(Redis 去重 + DB 唯一约束)防重放、状态机防乱序;MQ 消费者记得按业务键做幂等。

笔记加载中…