重复提交拦得住:Spring Boot + Redis 实现接口幂等
【摘要】 用 Spring Boot 配合 Redis 的 SETNX 在接口入口占坑,拦掉网络重试和手抖双击造成的重复提交,并讲清边界情况。
前端双击、网络超时重试、网关重发——这些都会让同一个请求到后端两三次。如果这是个"下单"“转账"接口,重复处理就是真金白银的损失。幂等(idempotent)说的就是"同一个请求跑一次和跑多次结果一样”。我用 Spring Boot + Redis 给关键接口加了一道幂等闸,做法记一下。

一、重复提交从哪来
- 用户手快点了两次提交按钮;
- 客户端超时,自动重试了一次;
- 中间代理或网关因为没收到响应而重发。
这些请求业务字段完全一样,光靠后端逻辑很难区分。所以在入口用"唯一业务号"占个坑,第二次进来发现坑被占了,直接拒绝,是最直接的办法。
二、用 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)