Java防止重复提交全解析:原理、场景与实战方案

一、什么是重复提交?

重复提交指用户在短时间内多次发送相同请求到服务端,导致数据被多次处理的现象。常见场景包括:

  • 用户快速点击两次提交按钮
  • 网络延迟导致客户端重复发送请求
  • 浏览器刷新/后退后重新提交表单

危害

  • 数据库产生重复数据(如重复订单)
  • 业务逻辑异常(如积分被多次扣除)
  • 服务器资源浪费,影响系统性能

二、重复提交的常见原因

1. 用户操作层面

  • 多次点击提交按钮:网络卡顿、信号差时,尤其是前端未做防抖处理时
  • 浏览器行为:刷新、回退后重新提交
  • 弱网环境重试:客户端自动重发未收到响应的请求

2. 系统设计层面

  • 非幂等接口设计:相同请求多次执行结果不一致
  • 缺乏并发控制:未对并发请求做同步处理

三、解决方案分类

1. 前端控制(辅助方案)

// 提交后禁用按钮
document.getElementById("submitBtn").disabled = true; 

// 使用防抖函数(500ms内只执行一次)
let isSubmitting = false;
function submitForm() {
  if (isSubmitting) return;
  isSubmitting = true;
  // 提交逻辑...
}

适用场景:简单表单提交
缺点:可通过禁用JS或直接调用API绕过


2. Token令牌机制(推荐)

实现步骤:
  1. 生成Token:服务端生成唯一Token存入Session/Redis
  2. 传递Token:将Token放入表单隐藏域或请求头
  3. 验证Token:服务端校验Token是否存在且匹配
  4. 销毁Token:处理成功后立即移除Token
// Spring Boot示例
@GetMapping("/form")
public String showForm(Model model, HttpSession session) {
    String token = UUID.randomUUID().toString();
    session.setAttribute("formToken", token);
    model.addAttribute("token", token);
    return "form-page";
}

@PostMapping("/submit")
public ResponseEntity<?> submitForm(@RequestParam String token, HttpSession session) {
    String serverToken = (String) session.getAttribute("formToken");
    if (serverToken == null || !serverToken.equals(token)) {
        return ResponseEntity.badRequest().body("重复提交!");
    }
    session.removeAttribute("formToken");
    // 处理业务逻辑...
    return ResponseEntity.ok("提交成功");
}

优点:安全性高,可防御恶意重复提交
缺点:需维护Token状态,分布式环境下需用Redis


3. 基于AOP+注解的防重提交(生产推荐)

3.1 自定义注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoRepeatSubmit {
    int lockTime() default 5; // 锁定时间(秒)
    String key() default ""; // 自定义业务键
}
3.2 AOP切面实现
@Aspect
@Component
public class RepeatSubmitAspect {
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    @Around("@annotation(noRepeatSubmit)")
    public Object around(ProceedingJoinPoint pjp, NoRepeatSubmit noRepeatSubmit) throws Throwable {
        ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
        HttpServletRequest request = attributes.getRequest();

        String key = buildKey(request, noRepeatSubmit);
        if (redisTemplate.hasKey(key)) {
            throw new RuntimeException("请勿重复提交");
        }

        redisTemplate.opsForValue().set(key, "", noRepeatSubmit.lockTime(), TimeUnit.SECONDS);
        return pjp.proceed();
    }

    private String buildKey(HttpServletRequest request, NoRepeatSubmit annotation) {
        return String.format("submit:lock:%s:%s",
                annotation.key(),
                request.getSession().getId() + ":" + request.getRequestURI());
    }
}
3.3 使用示例
@PostMapping("/order")
@NoRepeatSubmit(lockTime = 10, key = "createOrder")
public ApiResult createOrder(@RequestBody OrderDTO dto) {
    // 创建订单逻辑...
}

技术栈:Spring AOP + Redis
优点

  • 通过注解灵活控制接口防重
  • 支持分布式环境
  • 可扩展自定义防重策略

4. 数据库层防护

4.1 唯一索引
ALTER TABLE orders ADD UNIQUE KEY uk_order_no (order_no);
4.2 乐观锁
// 更新时校验版本号
UPDATE account 
SET balance = balance - 100, version = version + 1 
WHERE id = 123 AND version = 1;

适用场景

  • 核心业务数据防重(如订单号)
  • 高并发扣减类操作

四、方案选型建议

方案适用场景优点缺点
前端控制简单表单实现简单安全性低
Token机制传统Web表单安全性较好需维护Session/Redis
AOP+Redis分布式系统接口灵活、可扩展性强增加Redis依赖
数据库约束核心数据强一致性要求数据层最终保障增加数据库压力

五、幂等性设计最佳实践

  1. HTTP方法规范
    • GET:天然幂等
    • PUT/DELETE:应实现幂等
    • POST:需业务层保证
  2. 幂等Token设计
// 生成全局唯一业务ID
String bizId = IdWorker.get32UUID(); 
  1. 请求签名验证
String sign = DigestUtils.md5Hex(requestParam + timestamp + secretKey);

六、测试与验证

使用JMeter模拟并发提交:

<!-- JMeter测试计划示例 -->
<ThreadGroup>
  <LoopController loops="5" />
  <HTTPSampler>
    <POST>/api/submit</POST>
    <Arguments>
      <Argument name="data" value="test_data" />
    </Arguments>
  </HTTPSampler>
</ThreadGroup>

预期结果:多个请求中仅第一个成功,后续返回防重提示


总结

防止重复提交需从前端、服务端到数据库多层防护。对于Java技术栈,推荐组合使用:

  1. 前端防抖 + Token机制 处理基础场景
  2. AOP+Redis注解 实现分布式接口防护
  3. 数据库唯一约束 作为最终保障

根据实际业务场景选择合适的组合方案,可有效提升系统健壮性。示例代码已验证,可直接应用于生产环境。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

行者无疆1982

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值