本文目录导读:

Java 中实现统一的防重提交(防止重复请求)的核心思想是:利用请求的唯一标识 (如用户ID+接口+参数生成的MD5,或者前端生成的唯一请求ID),在短时间内(如 1-3 秒)对此标识进行判重与加锁。
为了实现 统一(即对业务代码侵入小,通用性强),通常采用 注解 + AOP(Aspect Oriented Programming,面向切面编程) 的方式,并结合 Redis(内存数据库) 作为高性能的锁存储。
以下是构建统一防重提交流程的完整方案:
核心设计思路
- 定义注解:创建一个
@RepeatSubmit注解,用于标记需要防重的方法。 - 切面处理:通过 AOP 拦截被注解标记的方法,在方法执行前进行校验。
- 生成锁Key:结合请求上下文(Token、IP、URL、参数)生成唯一的锁ID。
- 执行加锁:使用 Redis 的
SET NX EX命令(原子操作)尝试加锁。 - 请求放行:如果加锁成功(返回OK),则执行业务逻辑;如果加锁失败(已存在),则直接返回“请求频繁”或“重复提交”的异常/提示。
关键技术选型
- 存储介质:Redis(最佳实践),因为它支持分布式、原子操作、自动过期。
- 锁类型:Redis 的分布式锁(基于
SETNX命令)。 - 前端配合(可选但推荐):前端在请求头或参数中携带
requestId(UUID,通用唯一识别码),后端以此为主键,可以更精准地防止网络重试导致的数据重复。
具体实现步骤(代码示例)
引入依赖(Maven/Gradle)
<!-- Spring Boot AOP -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- Spring Boot Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
定义注解 @RepeatSubmit
import java.lang.annotation.*;
import java.util.concurrent.TimeUnit;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface RepeatSubmit {
/**
* 防重锁的时间间隔(秒)
* 在此时间内,相同标识的请求将被拦截
*/
int interval() default 3;
/**
* 提示信息
*/
String message() default "操作频率过快,请稍后再试!";
/**
* 锁的粒度模式
* PARAM: 基于请求参数(适合写操作)
* TOKEN: 基于用户Token + 请求路径(适合通用场景)
* GLOBAL: 全局锁,只基于请求路径
*/
LockMode mode() default LockMode.TOKEN;
enum LockMode {
PARAM, TOKEN, GLOBAL
}
}
核心 AOP 切面实现
这是防重提交流程的心脏,负责拦截、加锁、校验。
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.servlet.http.HttpServletRequest;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.concurrent.TimeUnit;
@Slf4j
@Aspect
@Component
public class RepeatSubmitAspect {
@Autowired
private StringRedisTemplate redisTemplate;
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, RepeatSubmit noRepeatSubmit) throws Throwable {
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
Method method = signature.getMethod();
// 1. 获取注解配置
int interval = noRepeatSubmit.interval();
String message = noRepeatSubmit.message();
RepeatSubmit.LockMode mode = noRepeatSubmit.mode();
// 2. 生成唯一的锁 Key
String lockKey = generateLockKey(joinPoint, method, mode, noRepeatSubmit);
// 3. 尝试加锁(Redis SET NX EX 原子操作)
// key 不存在则设置,并设置过期时间;返回 true 表示加锁成功
boolean locked = Boolean.TRUE.equals(
redisTemplate.opsForValue().setIfAbsent(lockKey, "1", interval, TimeUnit.SECONDS)
);
if (!locked) {
// 4. 加锁失败,说明重复提交
log.warn("检测到重复提交请求, key: {}", lockKey);
// 这里可以抛出异常,或者返回统一结果
throw new RuntimeException(message);
// 或者:return Result.error(message);
}
// 5. 加锁成功,执行业务逻辑
try {
return joinPoint.proceed();
} catch (Exception e) {
// 业务执行异常,需要释放锁吗?通常不需要,等待自动过期即可,防止锁误删
throw e;
}
// 注意:这里故意不手动释放锁
// 因为防重锁的生命周期就是 interval 秒,到期自动释放,确保在这段时间内不重复提交。
}
/**
* 生成锁 Key 的策略
*/
private String generateLockKey(ProceedingJoinPoint joinPoint, Method method, RepeatSubmit.LockMode mode, RepeatSubmit annotation) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
StringBuilder key = new StringBuilder("repeat:submit:");
// 1. 固定前缀:请求路径
key.append(request.getRequestURI());
switch (mode) {
case TOKEN:
// 最常见:基于用户身份(从请求头或JWT中获取UserID/Token) + 请求路径
String userId = request.getHeader("Authorization"); // 假设Token放在Authorization头
// 实际开发中应解析Token获取userId,或直接使用sessionId
key.append(":").append(userId != null ? userId : request.getSession().getId());
break;
case PARAM:
// 最严格:基于请求参数(将参数JSON序列化作为key的一部分)
// 注意:参数顺序可能不同,建议对参数Map排序后转为Json
Object[] args = joinPoint.getArgs();
// 生产环境建议使用更复杂的序列化(如取参数的HashCode)
key.append(":").append(Arrays.deepHashCode(args));
break;
case GLOBAL:
// 全局锁,只基于URI,不区分用户(极少使用,如秒杀前端限流)
// 不添加额外后缀
break;
}
return key.toString();
}
}
使用示例
直接在 Controller 层方法上添加 @RepeatSubmit 注解即可。
@RestController
@RequestMapping("/order")
public class OrderController {
@PostMapping("/create")
@RepeatSubmit(interval = 3, message = "订单正在处理中,请勿重复提交!")
public Result createOrder(@RequestBody OrderReq req) {
// 业务逻辑(创建订单)
return Result.success("下单成功");
}
@PostMapping("/pay")
@RepeatSubmit(interval = 5, mode = RepeatSubmit.LockMode.PARAM, message = "支付请求正在处理,请勿重复操作")
public Result pay(@RequestBody PayReq req) {
// 支付逻辑
return Result.success("支付成功");
}
}
统一异常处理
当切面检测到重复提交并抛出 RuntimeException 时,需要 全局异常处理器 捕获并转化为友好的前端提示,避免直接返回500。
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(RuntimeException.class)
public Result handleRepeatSubmitException(RuntimeException e) {
// 判断是否是重复提交异常
if (e.getMessage().contains("操作频率过快") || e.getMessage().contains("重复提交")) {
return Result.error(429, e.getMessage()); // 429 Too Many Requests
}
return Result.error(500, "系统异常");
}
}
关键问题与优化
锁的释放时机
- Q:业务执行成功后,需要立即删除 Redis Key 吗?
- A:不建议,防重提交流程的业务含义是“间隔期内禁止重复操作”,如果立即删除,用户快速点击两次,第一次执行完后锁消失,第二次点击可能依然能打进来(假如业务处理耗时很短),正确的做法是利用 过期时间 做硬性隔离,过期前无论如何都不允许再次提交。
基于Token的场景
- 在实际开发中,强烈建议前端在请求头中加入
X-Request-Id(UUID),后端 AOP 将requestId作为锁 Key,这是防重最强大的方式,因为即使是同一个用户两次完全相同的操作,只要 UUID 不同,也能确保不误拦截。
并发极高的场景(如秒杀)
interval设置为1秒,QPS(每秒请求数)极高,SET NX会成为瓶颈,可以考虑引入 Lua 脚本 进行更复杂的原子判断,或者使用 本地锁(Guava Cache + 布隆过滤器) 前置过滤,再用 Redis 兜底。
- 如何统一:通过 自定义注解 + AOP + Redis。
- 核心流程:
- 生成唯一 Key(URL + Token/UserID + 参数)。
- 尝试 Redis
SET NX EX。 - 成功:执行业务。
- 失败:抛出异常/返回提示。
这种方案对业务代码完全无侵入,只需在目标方法上加上 @RepeatSubmit 注解,即可完成统一的防重提交流程,支持分布式部署,且性能高效。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播