🔒 防重复提交策略
一、使用 Token 机制(防重令牌机制)
适合表单提交场景,强一致性,推荐做法之一。
原理:
前端提交请求前,先从后端获取一个唯一
token。提交时带上该 token,后端验证是否使用过。
如果已使用,拒绝本次提交;否则记录 token 并执行提交。
示例代码:
@PostMapping("/submit")
public ResponseEntity<?> submit(@RequestHeader("X-Form-Token") String token) {
if (redisService.exists(token)) {
return ResponseEntity.status(HttpStatus.CONFLICT).body("请勿重复提交");
}
// 标记已处理,设置一定过期时间
redisService.set(token, "used", 5 * 60); // 5分钟过期
// 执行业务逻辑
doBusiness();
return ResponseEntity.ok("提交成功");
}二、使用注解 + AOP(统一拦截重复提交)
适合 Spring Boot 项目,集中处理,业务无侵入性。
步骤:
自定义
@NoRepeatSubmit注解。使用 AOP 拦截请求(结合请求唯一标识如 IP+URI+参数+用户ID)在一定时间内是否重复。
简单实现:
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
int timeout() default 5; // 多少秒内不能重复
}
@Aspect
@Component
public class NoRepeatSubmitAspect {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
HttpServletRequest request = RequestContextHolderUtil.getRequest();
String userId = getUserId(request); // 自行定义
String uri = request.getRequestURI();
String key = "repeat_submit:" + userId + ":" + uri;
// 如果已存在,拦截
if (Boolean.TRUE.equals(redisTemplate.hasKey(key))) {
throw new BizException("请勿重复提交");
}
// 设置短期有效标志
redisTemplate.opsForValue().set(key, "1", Duration.ofSeconds(noRepeatSubmit.timeout()));
return pjp.proceed();
}
}三、数据库层加唯一约束(最终兜底防线)
适合关键数据不能重复的情况,如提交订单、报名记录。
举例:
如果是报名系统,不允许重复报名:
ALTER TABLE signup ADD UNIQUE(user_id, activity_id);然后捕获数据库 DuplicateKeyException 做业务提示。
四、使用分布式锁(Redisson / RedisLock)
适合高并发,控制同一用户或资源短时间内只能执行一次操作。
RLock lock = redissonClient.getLock("submit:lock:" + userId);
if (lock.tryLock(3, 10, TimeUnit.SECONDS)) {
try {
// 执行业务逻辑
} finally {
lock.unlock();
}
} else {
throw new BizException("操作频繁,请稍后再试");
}事例
import java.lang.annotation.*;
/**
* @author : 小生
* @description : 用于防止重复提交的注解
* @createDate : 2025/7/22 10:08
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NoRepeatSubmit {
/**
* 限制时间,单位:秒
*/
int timeout() default 1;
}import com.asdre.framework.common.security.SecurityUtils;
import com.asdre.framework.exception.BizException;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import java.time.Duration;
import java.util.Arrays;
/**
* @author : 小生
* @description :
* @createDate : 2025/7/22 10:08
*/
@Slf4j
@Aspect
@Component
public class NoRepeatSubmitAspect {
@Resource
private RedisTemplate<String, String> redisTemplate;
@Around("@annotation(noRepeatSubmit)")
public Object around(ProceedingJoinPoint joinPoint, NoRepeatSubmit noRepeatSubmit) throws Throwable {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes == null) {
return joinPoint.proceed();
}
HttpServletRequest request = attributes.getRequest();
// 简化版,真实项目建议获取用户ID或 Token
Long userId = SecurityUtils.getUserId();
String uri = request.getRequestURI();
String args = Arrays.toString(joinPoint.getArgs());
String key = "repeat_submit:" + userId + ":" + uri + ":" + args.hashCode();
int timeout = noRepeatSubmit.timeout();
Boolean exists = redisTemplate.hasKey(key);
if (Boolean.TRUE.equals(exists)) {
log.warn("重复提交拦截: {}", key);
throw new BizException("点击过快...");
}
// 设置锁标记,自动过期
redisTemplate.opsForValue().set(key, "1", Duration.ofSeconds(timeout));
return joinPoint.proceed();
}
}/**
* 在Controller测试
*
* @param req
* @return
*/
@PostMapping("/answer")
@NoRepeatSubmit(timeout = 1)
public AppResponse<SsStartVO> answer(@Validated @RequestBody AnswerReq req) {
return AppResponse.success(ssEvaService.answer(req));
}