高并发下如何避免重复提交表单?一线 Java 工程师的实战经验分享

高并发下如何避免重复提交表单?一线 Java 工程师的实战经验分享

引言:

"为什么用户点击一次提交,系统却生成了两笔订单?"

"为什么我加了锁,还是出现了重复支付?"

"为什么并发一上来,接口就乱套了?"

如果你在开发高并发系统、秒杀活动、支付接口、订单系统时碰到这种问题,那这篇文章你一定不能错过。作为一名从事 Java 后端开发 8 年的工程师,我将从实际业务出发,教你如何在高并发场景下彻底解决重复提交表单的问题。


🧭 一、业务背景与问题描述

1.1 重复提交的真实场景

在日常开发中,以下场景都可能导致重复提交:

  • 用户点击"提交订单"按钮多次(网络慢、页面卡顿)
  • 前端因网络异常自动重试请求
  • 支付系统中用户刷新页面重复支付
  • 后端服务处理较慢,用户误以为没提交成功

这些问题非常常见,却极具"杀伤力":轻则用户体验差,重则导致资金损失、库存异常、数据错乱。


🔍 二、重复提交的常见原因分析

原因 场景说明
表单未限制点击 用户多次点击提交按钮
接口无幂等控制 多次请求生成多个订单
分布式部署无状态 Session状态无法共享
中间件重试机制 Nginx、API Gateway 自动重试

🎯 三、如何解决重复提交问题?

✅ 原则:前端防抖 + 服务端幂等性保证

虽然前端可以做一些防抖处理,但服务端控制才是最后的防线 。尤其在高并发系统中,服务端必须保证:相同请求不会被处理两次。

常见解决方案对比:

方案 优点 缺点
数据库唯一约束 简单可靠 侵入性强,可能影响性能
接口幂等ID 解耦,灵活 需要调用方配合
Token机制(一次性Token) 轻量、独立、通用 依赖缓存系统
Redis分布式锁 精准控制 实现复杂、需注意死锁

✅ 四、最佳实践:Token机制防重复提交(基于Redis)

原理说明:

  1. 每次表单提交前,客户端先从服务器获取一个唯一 Token。

  2. 服务端将 Token 存入 Redis,设置过期时间。

  3. 客户端提交表单时,携带该 Token。

  4. 服务端校验 Token 是否存在:

    • 存在 ➝ 删除 Token ➝ 执行业务逻辑;
    • 不存在 ➝ 拒绝请求(说明是重复提交)。

🧪 五、代码实战:Spring Boot + Redis 实现一次性Token机制

5.1 获取Token接口

less 复制代码
@RestController
@RequestMapping("/api/token")
public class TokenController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 生成一次性Token(有效期5分钟)
     */
    @GetMapping("/generate")
    public ResponseEntity<String> generateToken() {
        String token = UUID.randomUUID().toString();
        String key = "form:token:" + token;

        // 存入Redis,设置5分钟过期
        redisTemplate.opsForValue().set(key, "1", 5, TimeUnit.MINUTES);

        return ResponseEntity.ok(token);
    }
}

5.2 表单提交接口

less 复制代码
@RestController
@RequestMapping("/api/order")
public class OrderController {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    /**
     * 提交订单接口。使用Token防止重复提交
     */
    @PostMapping("/submit")
    public ResponseEntity<String> submitOrder(@RequestBody OrderRequest request) {
        String tokenKey = "form:token:" + request.getToken();

        // 使用 Redis 的 delete 操作作为原子校验
        Boolean success = redisTemplate.delete(tokenKey);

        if (Boolean.FALSE.equals(success)) {
            return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                    .body("请勿重复提交!");
        }

        // TODO: 执行核心业务逻辑(如创建订单、扣减库存)
        // 这里应该保证业务也具有幂等性(如幂等ID)

        return ResponseEntity.ok("订单提交成功");
    }
}

5.3 请求参数对象

arduino 复制代码
@Data
public class OrderRequest {
    private String token;
    private String productId;
    private int quantity;
}

🧠 六、关键技术细节说明

  • ✅ Token使用一次即删除,防止重复使用
  • ✅ Redis支持原子操作,高效线程安全
  • ✅ 可扩展性强:支持多业务共用
  • ✅ 无状态设计,适用于分布式部署

⚙️ 七、进阶优化建议

7.1 拦截器统一处理Token验证

使用Spring的HandlerInterceptor统一拦截提交请求,避免每个Controller重复代码。

vbnet 复制代码
@Component
public class TokenInterceptor implements HandlerInterceptor {

    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        String token = request.getHeader("X-Form-Token");
        if (StringUtils.isEmpty(token)) {
            response.setStatus(400);
            response.getWriter().write("缺少Token");
            return false;
        }

        String key = "form:token:" + token;

        Boolean success = redisTemplate.delete(key);
        if (Boolean.FALSE.equals(success)) {
            response.setStatus(400);
            response.getWriter().write("请勿重复提交!");
            return false;
        }

        return true;
    }
}

配置WebMvcConfigurer注册拦截器:

typescript 复制代码
@Configuration
public class WebConfig implements WebMvcConfigurer {
    @Autowired
    private TokenInterceptor tokenInterceptor;

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(tokenInterceptor)
                .addPathPatterns("/api/order/**");
    }
}

7.2 Token粒度控制

  • 按照用户维度+Token :form:token:{userId}:{token}
  • 按照业务维度划分:不同业务使用不同前缀,避免冲突

7.3 Token安全性设计

  • 可使用JWT或签名机制加密Token,防止伪造
  • 可结合用户会话Token进行验证

📌 八、总结

重复提交表单在系统低并发时可能只是个小问题,但一旦并发上来,就可能造成灾难级后果。

本篇文章结合项目实战经验,从业务痛点到技术方案,详细讲解了如何通过Redis + Token机制实现服务端防重复提交的统一解决方案。

✅ 简单易用

✅ 分布式友好

✅ 拓展性强

✅ 性能高效

相关推荐
喵个咪7 分钟前
GoWind CMS 拆解:给 NL2SQL 上四把锁——智能问数的安全边界
后端
喵个咪15 分钟前
GoWind CMS 拆解:在 Go 里给 TipTap 搭一套 Yjs 协同后端
后端·go·cms
开开心心就好21 分钟前
课堂随机点名工具,绿色版免安装双击即用
java·服务器·前端·ffmpeg·音视频·perl·symfony
不像程序员的程序媛21 分钟前
springboot支持的web容器
前端·spring boot·后端
喵个咪22 分钟前
GoWind Admin 拆解:一个 Sender 接口接 39 个通知渠道
后端·架构·go
code2cat26 分钟前
Java高级篇之Socket:从建立连接到完整收发一条消息
java·开发语言
PD我是你的真爱粉34 分钟前
成熟Agent系统设计深度研究报告
java·大数据·人工智能
用户693717500138435 分钟前
当你向领导申请 AI 编程工具预算时,如果领导追问“能提效多少,有量化数据吗”,你该怎么回答?
android·后端·ios
喵个咪37 分钟前
GoWind CMS 拆解:社交分发引擎——10 平台适配器、状态闸门与自愈定时任务
后端·go·cms
lichenyang4531 小时前
技术穿刺怎么做?以企业知识库问答为例,验证风险、方案和投入
前端·后端