背景
在订单创建、支付等接口中,用户可能因网络延迟重复点击按钮,导致后端收到多次相同请求,产生多条重复订单。幂等性 指同一个请求无论执行多少次,对资源状态的影响都相同。本文将教你基于 Redis + 自定义注解 优雅实现防重复提交。
实现原理
客户端请求前先向服务端申请一个 唯一 token(或前端生成 UUID)
请求时将 token 放入 Header
服务端接收到请求,检查 Redis 中是否存在该 token
如果存在,则删除 token 并执行业务逻辑;如果不存在,则说明是重复请求,直接拒绝
代码实现
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Idempotent {
String value() default "idempotent_token";
long expire() default 60; // token有效期(秒)
}
@RestController
public class TokenController {
@Autowired
private StringRedisTemplate redisTemplate;
@GetMapping("/token")
public String getToken() {
String token = UUID.randomUUID().toString();
// 存储到Redis,设置过期时间60秒
redisTemplate.opsForValue().set(token, "1", 60, TimeUnit.SECONDS);
return token;
}
}
@Component
public class IdempotentInterceptor implements HandlerInterceptor {
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
if (!(handler instanceof HandlerMethod)) {
return true;
}
HandlerMethod handlerMethod = (HandlerMethod) handler;
Idempotent idempotent = handlerMethod.getMethodAnnotation(Idempotent.class);
if (idempotent == null) {
return true;
}
String token = request.getHeader("idempotent-token");
if (StringUtils.isEmpty(token)) {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\\"code\\":400,\\"msg\\":\\"缺少幂等token\\"}");
return false;
}
// 使用Redis的原子操作:删除成功则说明token有效且未被使用
Boolean deleted = redisTemplate.delete(token);
if (Boolean.TRUE.equals(deleted)) {
return true; // 首次请求,继续执行
} else {
response.setContentType("application/json;charset=utf-8");
response.getWriter().write("{\\"code\\":409,\\"msg\\":\\"重复请求,请勿重试\\"}");
return false;
}
}
}
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Autowired
private IdempotentInterceptor idempotentInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(idempotentInterceptor).addPathPatterns("/**");
}
}
@PostMapping("/order")
@Idempotent(expire = 120)
public Result createOrder(@RequestBody OrderVo orderVo) {
// 业务逻辑(只有第一次请求能到达这里)
orderService.create(orderVo);
return Result.success();
}
测试流程
前端先调用 GET /token 获取 token
调用 POST /order 时,Header 带上 idempotent-token: xxx
第一次请求成功,Redis 中的 token 被删除
第二次相同 token 请求,拦截器返回“重复请求”
进阶优化
使用 Lua 脚本确保“判断存在并删除”的原子性(但 redisTemplate.delete 本身是原子的)
支持 token 有效期与业务时间匹配:注解中设置 expire 时间应大于业务最大处理时间
分布式环境:完全适用,因为 Redis 是共享的
总结
通过注解 + Redis 的方式,我们几乎零侵入地实现了接口幂等性。这种方式比前端禁用按钮更可靠,因为后端从根本上杜绝了重复提交。生产环境中还可以结合 @Around AOP 来实现更灵活的控制。






