重复提交拦得住:Spring Boot + Redis 实现接口幂等

举报
静水流深-云海 发表于 2026/09/15 14:06:56 2026/09/15
【摘要】 用 Spring Boot 配合 Redis 的 SETNX 在接口入口占坑,拦掉网络重试和手抖双击造成的重复提交,并讲清边界情况。

前端双击、网络超时重试、网关重发——这些都会让同一个请求到后端两三次。如果这是个"下单"“转账"接口,重复处理就是真金白银的损失。幂等(idempotent)说的就是"同一个请求跑一次和跑多次结果一样”。我用 Spring Boot + Redis 给关键接口加了一道幂等闸,做法记一下。

image.png

一、重复提交从哪来

  • 用户手快点了两次提交按钮;
  • 客户端超时,自动重试了一次;
  • 中间代理或网关因为没收到响应而重发。

这些请求业务字段完全一样,光靠后端逻辑很难区分。所以在入口用"唯一业务号"占个坑,第二次进来发现坑被占了,直接拒绝,是最直接的办法。

二、用 Redis 令牌拦一道

核心是用 Redis 的 SETNX(set if not exist)原子操作:第一次写入成功就放行,重复请求写入失败就拦掉。Spring 的 RedisTemplate 对应方法就是 setIfAbsent:

@Service
public class IdempotentService {

    @Autowired
    private StringRedisTemplate redis;

    // 返回 true 表示是首次请求,可以放行
    public boolean checkAndMark(String bizKey, long ttlSeconds) {
        Boolean ok = redis.opsForValue()
                .setIfAbsent("idem:" + bizKey, "1",
                             Duration.ofSeconds(ttlSeconds));
        return Boolean.TRUE.equals(ok);
    }
}

在 Controller 里取业务唯一号(如订单号 + 用户 ID)做 key:

@PostMapping("/pay")
public ResponseEntity<?> pay(@RequestBody PayReq req) {
    String key = req.getUserId() + ":" + req.getOrderId();
    if (!idempotent.checkAndMark(key, 300)) {
        return ResponseEntity.status(409).body("重复请求,已忽略");
    }
    // 正常业务处理...
    return ResponseEntity.ok("ok");
}

TTL 设个合理值(如 5 分钟),避免 key 永久堆积。

三、注解还是拦截器

方式 侵入性 复用度
在 Controller 里直接调 高(每个接口写一遍) 低
自定义 @Idempotent 注解 + AOP 低(标注解即可) 高
拦截器统一处理 中 中

接口多的话,建议抽成注解 + AOP,一个注解标上去就生效,最省事。

四、几个边界情况

  • TTL 要比业务耗时更长:业务跑 10 秒、TTL 设 5 秒,正常请求都可能被自己后续的"重试"误伤。留足余量。
  • key 要够唯一:只拿用户 ID 当 key,会误拦同一用户的不同订单;要带上业务单号。
  • 分布式下必须用 Redis:单机用内存 Map 在多实例部署时各算各的,拦不住,Redis 才是共享的"坑位"。
  • 异常处理要回滚坑位:如果业务抛异常,考虑是否删除 key 允许重试,否则这次失败用户永远不能再提交。

总结

接口幂等不是高级概念,本质就是"用一把带超时的分布式锁占坑"。Redis 的 SETNX 原子性强、带过期、天然共享,是这件事的标准解法。等这个模式跑顺了,抽成 AOP 注解,后面任何怕重复的接口,标一行注解就稳了。比起事后对账补单,入口拦一道省下的麻烦多得多。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。