Spring Boot 接口防重实战:哈希 + 缓存方案,10 分钟快速集成
大家好,我是你的技术分享者。今天我们来聊聊一个在后端开发中非常实用的技巧——如何在 Spring Boot 中防止接口重复提交。重复提交往往会导致数据混乱,比如订单重复创建或数据库多条记录。别担心,这个方案简单高效,只需 10 分钟就能集成!我们将使用哈希算法结合缓存(如 Redis 或 Caffeine)来实现,代码和截图一应俱全,一起跟着做吧。
01 — 防重复提交说明
在实际系统中,重复提交是常见问题。例如,用户在下订单时,如果鼠标连击或网络抖动,就可能生成多个订单;或者在查询用户数据时,重复请求导致多条记录,引发错误。
防重复提交的核心是防止用户在短时间内对同一接口多次提交相同请求,从而避免数据重复或状态异常。通常,我们只在关键业务上应用,比如表单提交、订单创建、支付操作等。原理是通过唯一标识(如 Token 或请求参数哈希)记录请求状态,在有效时间内拒绝重复请求。
常见实现方式有三种:
-
前端控制:在按钮或页面添加 loading 效果,防止用户重复点击。但这依赖前端实现,不够可靠。
-
后端基于用户标识:根据用户 ID 或 Token 判断同一接口的多次提交。需要用户登录或唯一标识,适用场景有限。
-
后端基于请求内容:结合请求路径、方法和参数生成唯一键判断。这是最灵活的方式,不依赖前端或用户标识,适用于各种场景。
本文采用第三种方法:后端根据 URL 路径 + 请求方法 + 参数生成 SHA-256 哈希值作为缓存键,判断是否重复。SHA-256 算法输出固定 256 位哈希,具有高唯一性和抗碰撞性,无论输入多长,都能快速生成。
先来看整体流程图,便于理解整个机制。

02 — 具体实现
我们基于 Spring Boot 的 AOP(面向切面编程)来拦截请求,实现防重逻辑。支持 Redis(分布式缓存)和 Caffeine(本地缓存)两种存储方式。接下来一步步代码实现。
2.1 创建自定义注解
首先,定义一个注解 @PreventDuplicate,用于标注需要防重的 Controller 方法。可以配置过期时间、时间单位、关键字段和提示消息。
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PreventDuplicate {
/**
* 防重复提交时间(单位:秒)
*/
int expire() default 3;
/**
* 时间单位,默认秒
*/
TimeUnit timeUnit() default TimeUnit.SECONDS;
/**
* 设置作为key字段值
*/
String[] field() default {};
/**
* 防重复提交提示信息
*/
String message() default "请勿重复提交!";
}
这个注解灵活性高,能根据业务调整间隔时间或只用部分字段生成键。

2.2 AOP 拦截器实现
接下来,实现 AOP 切面类 PreventDuplicateAspect,它会拦截标注了注解的方法,生成哈希键并检查缓存。
import lombok.RequiredArgsConstructor;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
@Aspect
@Component
@RequiredArgsConstructor
public class PreventDuplicateAspect {
private final HttpServletRequest request;
private final DuplicateStorageFactory storageFactory;
@Around("@annotation(preventDuplicate)")
public Object handle(ProceedingJoinPoint joinPoint, PreventDuplicate preventDuplicate) throws Throwable {
// 请求方法
String method = request.getMethod();
// 请求地址
String uri = request.getRequestURI();
// 请求参数
String body = RequestParameterUtils.getAllParamsAsString(joinPoint, preventDuplicate.field());
// 拼接签名参数,生成唯一的key
String signSource = method + ":" + uri + ":" + body;
String key = DigestUtil.sha256Hex(signSource);
// 获取缓存实现
DuplicateStorage storage = storageFactory.getStorage();
if (storage.exists(key)) {
// 存在则返回错误提示
throw new RuntimeException(preventDuplicate.message());
}
// 写入缓存
storage.put(key, preventDuplicate.expire(), preventDuplicate.timeUnit());
// 执行原方法
return joinPoint.proceed();
}
}
这里的关键是生成 SHA-256 哈希键:结合方法、URI 和参数(支持全量或指定字段)。然后用缓存检查是否存在,若存在则抛异常;否则存入缓存并执行方法。
我们支持 Redis 和 Caffeine 两种缓存:

2.3 Controller 示例与测试
在 Controller 中直接添加注解:
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/demo")
public class DemoController {
@GetMapping("/hello")
@PreventDuplicate
public String hello(String name, String age, String address) {
return "防重复提交测试:" + name + " " + age + " " + address;
}
@PostMapping("/saveUserInfo")
@PreventDuplicate(expire = 5)
public String saveUserInfo(@RequestBody UserInfo userInfo) {
System.out.println(userInfo.toString());
return "请求时间:" + DateTime.now() + " 保存成功";
}
}
测试效果:如果在 5 秒内提交相同内容,会提示重复提交。

2.4 性能测试:大参数哈希生成
担心参数多影响性能?我们测试一个模拟上传 3 万字文章的接口:
@PostMapping("/saveContent")
@PreventDuplicate(expire = 10)
public String saveContent(@RequestBody ArticleDTO articleDTO) {
System.out.println(articleDTO.toString());
return "请求时间:" + DateTime.now() + " 保存成功";
}
在生成哈希时记录时间:
long startTime = System.currentTimeMillis();
String key = DigestUtil.sha256Hex(signSource);
long endTime = System.currentTimeMillis();
System.out.println("生成哈希值耗时:" + (endTime - startTime) + "ms");
结果:首次 9ms,后续基本 0ms。即使大参数,性能优秀!

03 — 总结
这个方案简单高效,利用哈希 + 缓存防重提交,支持 Redis 和 Caffeine。适用于各种关键接口,集成只需几分钟。完整代码已上传,有问题欢迎评论指正!
源码地址:https://gitee.com/QinXianZhong/debounce.git
喜欢的话,点个赞或关注哦~ 更多 Spring Boot 实战分享,敬请期待!
评论区