如何防止重复提交订单?
作者:Java后端开发工程师
一、背景介绍:为什么会产生重复提交?
在电商平台中,用户提交订单是一个非常敏感的动作。这通常涉及:
- 库存扣减
- 优惠券核销
- 支付下单
- 消息发送
但用户总喜欢:
- 点两次"提交订单"按钮
- 网络卡顿时刷新页面
- 使用浏览器回退再次提交
结果就是:重复提交订单,造成资源浪费,甚至业务损失!
二、问题分析:重复提交的常见场景
场景 | 示例 |
---|---|
用户行为 | 多次点击按钮、浏览器刷新 |
接口幂等性差 | 接口无幂等校验,每次都生成新订单 |
网络重试 | 客户端自动重发请求(如超时) |
分布式系统 | 多个节点并发处理同一订单请求 |
三、防止重复提交的核心原则
要解决重复提交问题,必须从接口幂等性 + 请求唯一性 + 服务端锁控制三方面入手:
- 控制请求的唯一标识(token/nonce)
- 对订单操作进行幂等处理
- 引入缓存或分布式锁限制重复提交
四、解决方案:基于Token机制 + Redis锁的防重复提交设计
✅ 设计思路:
- 前端在创建订单前从服务端获取一个唯一 token(防重复提交标识)
- 提交订单时将 token 附带传入
- 后端验证 token 是否存在(Redis)
- 校验通过 → 执行下单逻辑 → 删除 token
- 若 token 已被使用 → 拒绝重复提交
五、代码实现(Spring Boot + Redis)
1. 前端获取防重复提交 Token 接口
typescript
@RestController
@RequestMapping("/api/order")
public class OrderTokenController {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@GetMapping("/token")
public ResponseEntity<String> getToken() {
String token = UUID.randomUUID().toString();
String key = "order:token:" + token;
// 设置有效期5分钟
redisTemplate.opsForValue().set(key, "valid", Duration.ofMinutes(5));
return ResponseEntity.ok(token);
}
}
2. 提交订单接口(验证token + 删除token)
less
@RestController
@RequestMapping("/api/order")
public class OrderController {
@Autowired
private OrderService orderService;
@PostMapping("/submit")
public ResponseEntity<String> submitOrder(@RequestBody OrderRequest request,
@RequestHeader("X-Order-Token") String token) {
boolean success = orderService.submitOrder(request, token);
if (success) {
return ResponseEntity.ok("订单提交成功");
} else {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body("请勿重复提交订单");
}
}
}
3. OrderService 实现防重复提交逻辑
typescript
@Service
public class OrderService {
@Autowired
private RedisTemplate<String, String> redisTemplate;
/**
* 提交订单接口,防止重复提交
*/
public boolean submitOrder(OrderRequest request, String token) {
String redisKey = "order:token:" + token;
// 利用 Redis 的 delete + check 保证幂等性(原子性)
Boolean result = redisTemplate.delete(redisKey);
if (Boolean.TRUE.equals(result)) {
// token存在并删除 → 第一次提交
// 执行正常订单创建逻辑
createOrder(request);
return true;
} else {
// token 不存在 → 重复提交
return false;
}
}
private void createOrder(OrderRequest request) {
// 实际业务处理:生成订单号、校验库存、扣减库存、写库、发MQ等
System.out.println("处理订单:" + request);
}
}
4. 请求对象 OrderRequest 示例
kotlin
@Data
public class OrderRequest {
private Long userId;
private List<Long> productIds;
private BigDecimal totalAmount;
}
六、进阶优化建议
1. 使用 Lua 脚本保证 Redis 操作原子性
Redis
delete
操作不是强原子性的,建议使用 Lua 脚本执行 "判断 + 删除" 逻辑。
ini
// Lua 脚本实现原子删除
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
redisScript.setScriptText(script);
redisScript.setResultType(Long.class);
Long result = redisTemplate.execute(redisScript, Collections.singletonList(key), "valid");
2. 给订单接口加限流或熔断保护(如 Sentinel)
- 防止恶意刷接口
- 降低重复提交带来的系统压力
3. 数据库层幂等校验(双保险)
即便应用层失效,也可以通过数据库约束(如订单号唯一)+ INSERT IGNORE
或 ON DUPLICATE KEY
防止重复插入。
七、总结
面对用户重复提交订单的问题,我们不能只靠前端"禁用按钮"了,而是应该从后端保障:
- 请求唯一性
- 接口幂等性
- 服务端锁机制
✅ 实战建议:
- Redis 是处理幂等控制的利器
- token机制简单实用,适用于下单、支付、秒杀等场景
- 多层防御更安全:应用层 + 数据库层
📌 最后
这篇文章分享了我在实际项目中防止订单重复提交的完整方案,希望对你有所帮助!
如果你也在做订单系统、支付系统,欢迎留言交流你的经验。