防重复提交策略

Administrator
发布于 2025-07-22 / 0 阅读
0
0

防重复提交策略

🔒 防重复提交策略

场景

建议方案

表单提交

Token机制 / 注解+AOP

秒杀、报名等关键操作

数据库唯一键 / Redis分布式锁

所有接口统一处理

注解+AOP + Redis

需防短时间多次点击

前端节流 + 后端限制组合使用

一、使用 Token 机制(防重令牌机制)

适合表单提交场景,强一致性,推荐做法之一。

原理:

  1. 前端提交请求前,先从后端获取一个唯一 token

  2. 提交时带上该 token,后端验证是否使用过。

  3. 如果已使用,拒绝本次提交;否则记录 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 项目,集中处理,业务无侵入性。

步骤:

  1. 自定义 @NoRepeatSubmit 注解。

  2. 使用 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));
}


评论