高并发下如何避免重复提交表单?一线 Java 工程师的实战经验分享
引言:
"为什么用户点击一次提交,系统却生成了两笔订单?"
"为什么我加了锁,还是出现了重复支付?"
"为什么并发一上来,接口就乱套了?"
如果你在开发高并发系统、秒杀活动、支付接口、订单系统时碰到这种问题,那这篇文章你一定不能错过。作为一名从事 Java 后端开发 8 年的工程师,我将从实际业务出发,教你如何在高并发场景下彻底解决重复提交表单的问题。
🧭 一、业务背景与问题描述
1.1 重复提交的真实场景
在日常开发中,以下场景都可能导致重复提交:
- 用户点击"提交订单"按钮多次(网络慢、页面卡顿)
- 前端因网络异常自动重试请求
- 支付系统中用户刷新页面重复支付
- 后端服务处理较慢,用户误以为没提交成功
这些问题非常常见,却极具"杀伤力":轻则用户体验差,重则导致资金损失、库存异常、数据错乱。
🔍 二、重复提交的常见原因分析
原因 | 场景说明 |
---|---|
表单未限制点击 | 用户多次点击提交按钮 |
接口无幂等控制 | 多次请求生成多个订单 |
分布式部署无状态 | Session状态无法共享 |
中间件重试机制 | Nginx、API Gateway 自动重试 |
🎯 三、如何解决重复提交问题?
✅ 原则:前端防抖 + 服务端幂等性保证
虽然前端可以做一些防抖处理,但服务端控制才是最后的防线 。尤其在高并发系统中,服务端必须保证:相同请求不会被处理两次。
常见解决方案对比:
方案 | 优点 | 缺点 |
---|---|---|
数据库唯一约束 | 简单可靠 | 侵入性强,可能影响性能 |
接口幂等ID | 解耦,灵活 | 需要调用方配合 |
Token机制(一次性Token) | 轻量、独立、通用 | 依赖缓存系统 |
Redis分布式锁 | 精准控制 | 实现复杂、需注意死锁 |
✅ 四、最佳实践:Token机制防重复提交(基于Redis)
原理说明:
-
每次表单提交前,客户端先从服务器获取一个唯一 Token。
-
服务端将 Token 存入 Redis,设置过期时间。
-
客户端提交表单时,携带该 Token。
-
服务端校验 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机制实现服务端防重复提交的统一解决方案。
✅ 简单易用
✅ 分布式友好
✅ 拓展性强
✅ 性能高效