重复提交订单是电子商务、支付系统和在线服务中常见的难题,可能导致库存错误、财务异常或用户体验下降。重复提交通常由用户快速点击、浏览器刷新、网络重试或恶意操作引起。本文将分析重复提交订单的原因,提供多种防止重复提交的解决方案,并在 Spring Boot 3.2 中实现一个电商订单系统,集成 MySQL 8.4、Redis 分布式锁、AOP 监控和幂等性控制。本文目标是为开发者提供一份全面的中文技术指南,帮助在 2025 年的高并发场景下有效防止重复提交订单。
一、重复提交订单的背景与问题分析
1.1 重复提交的场景
- 用户行为:
- 快速点击“提交订单”按钮。
- 浏览器后退或刷新页面,重发请求。
- 网络问题:
- 网络延迟导致客户端重试。
- 服务端未及时响应,触发超时重试。
- 恶意攻击:
- 利用脚本模拟多次提交。
- 绕过前端校验重复请求。
- 系统设计缺陷:
- 缺少幂等性控制。
- 并发场景未加锁。
1.2 问题影响
- 业务逻辑错误:库存超卖、重复扣款。
- 用户体验下降:订单异常,需人工退款。
- 系统性能压力:重复请求增加数据库负载。
- 财务风险:重复订单导致资金核算错误。
1.3 解决方案目标
- 幂等性:相同请求多次提交,结果一致。
- 高性能:低延迟,适配高并发。
- 安全性:防止恶意重复提交。
- 易维护:代码清晰,易于扩展。
1.4 常见解决方案
- 前端控制:
- 禁用提交按钮。
- 防抖/节流限制点击。
- 后端校验:
- 数据库唯一约束。
- 幂等性 Token。
- 分布式锁:
- 使用 Redis 或 ZooKeeper 控制并发。
- 消息队列:
- 异步处理订单,检查重复。
- 乐观锁/悲观锁:
- 数据库锁机制防止并发写入。
本文选择 前端控制 + 幂等性 Token + Redis 分布式锁 的组合方案,结合 MySQL 8.4 的 JSON 功能和 Spring Boot 生态,适合高并发电商场景。
二、解决方案设计
2.1 技术栈
- Spring Boot 3.2:核心框架。
- MySQL 8.4:存储订单数据,利用 JSON 和窗口函数。
- Redis:分布式锁和幂等性 Token 存储。
- Redisson:实现分布式锁。
- AOP:监控重复提交和性能。
- Spring Security:保护 API 安全。
- ActiveMQ:异步日志记录。
2.2 防止重复提交的策略
- 前端控制:
- 提交按钮点击后禁用,成功后重置。
- 使用防抖限制快速点击。
- 幂等性 Token:
- 客户端请求时生成唯一 Token,服务端校验。
- Redis 存储 Token,设置 TTL(如 10 秒)。
- Redis 分布式锁:
- 使用 Redisson 锁定用户订单操作,防止并发。
- 数据库唯一约束:
- 订单表添加唯一索引,防止重复插入。
- AOP 监控:
- 记录重复提交尝试和性能指标。
2.3 流程
- 创建订单请求:
- 客户端生成 UUID 作为 Token,发送到服务端。
- 服务端校验 Token 是否在 Redis 中。
- 分布式锁:
- 以用户 ID 为 key 获取 Redisson 锁。
- 锁内校验订单是否重复(数据库查询)。
- 订单处理:
- 插入订单,依赖 MySQL 唯一约束。
- 删除 Redis Token,释放锁。
- 异步日志:
- 通过 ActiveMQ 记录操作日志。
- 监控:
- AOP 记录请求耗时和重复提交。
三、在 Spring Boot 中实现
以下是一个电商订单系统的实现,防止重复提交订单,集成 MySQL 8.4、Redis、Redisson 和 AOP。
3.1 环境搭建
3.1.1 配置步骤
-
创建 Spring Boot 项目:
- 使用 Spring Initializr 添加依赖:
spring-boot-starter-web
spring-boot-starter-data-jpa
spring-boot-starter-data-redis
mysql-connector-java
redisson-spring-boot-starter
spring-boot-starter-activemq
spring-boot-starter-aop
spring-boot-starter-security
<project> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.2.0</version> </parent> <groupId>com.example</groupId> <artifactId>order-deduplication-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>org.redisson</groupId> <artifactId>redisson-spring-boot-starter</artifactId> <version>3.23.2</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-activemq</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency> </dependencies> </project>
- 使用 Spring Initializr 添加依赖:
-
准备数据库和 Redis:
- MySQL 8.4:
CREATE DATABASE order_db; USE order_db; CREATE TABLE orders ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT, order_no VARCHAR(50) UNIQUE, product_id BIGINT, quantity INT, details JSON, created_at TIMESTAMP, INDEX idx_user_id (user_id) );
- Redis:启动 Redis 实例(默认端口 6379)。
- MySQL 8.4:
-
配置
application.yml
:spring: profiles: active: dev application: name: order-deduplication-demo datasource: url: jdbc:mysql://localhost:3306/order_db?useSSL=false&serverTimezone=UTC username: root password: root driver-class-name: com.mysql.cj.jdbc.Driver jpa: hibernate: ddl-auto: none show-sql: true redis: host: localhost port: 6379 activemq: broker-url: tcp://localhost:61616 user: admin password: admin server: port: 8081 management: endpoints: web: exposure: include: health,metrics redisson: single-server-config: address: redis://localhost:6379 logging: level: root: INFO com.example.demo: DEBUG
-
运行并验证:
- 启动 MySQL、Redis 和 ActiveMQ。
- 启动应用:
mvn spring-boot:run
。 - 检查日志,确认 Redisson 初始化。
3.1.2 原理
- MySQL 8.4:存储订单,JSON 字段保存动态数据,唯一约束防止重复。
- Redis:存储幂等性 Token,Redisson 实现分布式锁。
- ActiveMQ:异步记录操作日志。
- AOP:监控重复提交和性能。
3.1.3 优点
- 高效防止重复提交。
- 支持高并发。
- 集成 MySQL 8.4 特性。
3.1.4 缺点
- 配置复杂,需多组件协调。
- 分布式锁增加少量延迟。
- Redis 故障需降级方案。
3.1.5 适用场景
- 电商订单创建。
- 支付系统。
- 高并发表单提交。
3.2 实现订单创建与防重复提交
实现订单创建接口,防止重复提交。
3.2.1 配置步骤
-
实体类(
Order.java
):package com.example.demo.entity; import jakarta.persistence.Entity; import jakarta.persistence.Id; import java.time.LocalDateTime; @Entity public class Order { @Id private Long id; private Long userId; private String orderNo; private Long productId; private Integer quantity; private String details; private LocalDateTime createdAt; // Getters and Setters public Long getId() { return id; } public void setId(Long id) { this.id = id; } public Long getUserId() { return userId; } public void setUserId(Long userId) { this.userId = userId; } public String getOrderNo() { return orderNo; } public void setOrderNo(String orderNo) { this.orderNo = orderNo; } public Long getProductId() { return productId; } public void setProductId(Long productId) { this.productId = productId; } public Integer getQuantity() { return quantity; } public void setQuantity(Integer quantity) { this.quantity = quantity; } public String getDetails() { return details; } public void setDetails(String details) { this.details = details; } public LocalDateTime getCreatedAt() { return createdAt; } public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; } }
-
Repository(
OrderRepository.java
):package com.example.demo.repository; import com.example.demo.entity.Order; import org.springframework.data.jpa.repository.JpaRepository; public interface OrderRepository extends JpaRepository<Order, Long> { boolean existsByOrderNo(String orderNo); }
-
服务层(
OrderService.java
):package com.example.demo.service; import com.example.demo.entity.Order; import com.example.demo.repository.OrderRepository; import org.redisson.api.RLock; import org.redisson.api.RedissonClient; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.jms.core.JmsTemplate; import org.springframework.stereotype.Service; import java.time.LocalDateTime; import java.util.UUID; import java.util.concurrent.TimeUnit; @Service public class OrderService { private static final Logger logger = LoggerFactory.getLogger(OrderService.class); private static final String TOKEN_PREFIX = "order:token:"; private static final String LOCK_PREFIX = "lock:order:user:"; @Autowired private OrderRepository orderRepository; @Autowired private RedisTemplate<String, Object> redisTemplate; @Autowired private RedissonClient redissonClient; @Autowired private JmsTemplate jmsTemplate; public String generateToken() { String token = UUID.randomUUID().toString(); redisTemplate.opsForValue().set(TOKEN_PREFIX + token, "1", 10, TimeUnit.SECONDS); return token; } public void createOrder(Order order, String token, Long userId) { String tokenKey = TOKEN_PREFIX + token; String lockKey = LOCK_PREFIX + userId; RLock lock = redissonClient.getLock(lockKey); try { // 校验 Token Boolean tokenExists = redisTemplate.hasKey(tokenKey); if (tokenExists == null || !tokenExists) { logger.warn("Invalid or expired token: {}", token); throw new RuntimeException("Invalid or expired token"); } // 获取分布式锁 if (lock.tryLock(10, 30, TimeUnit.SECONDS)) { try { // 校验订单是否重复 if (orderRepository.existsByOrderNo(order.getOrderNo())) { logger.warn("Duplicate order detected: {}", order.getOrderNo()); throw new RuntimeException("Duplicate order"); } // 保存订单 order.setCreatedAt(LocalDateTime.now()); order.setUserId(userId); orderRepository.save(order); // 删除 Token redisTemplate.delete(tokenKey); // 异步记录日志 jmsTemplate.convertAndSend("order-log-queue", "Created order: " + order.getOrderNo()); logger.info("Order created: {}", order.getOrderNo()); } finally { if (lock.isHeldByCurrentThread()) { lock.unlock(); } } } else { logger.warn("Failed to acquire lock for user: {}", userId); throw new RuntimeException("System busy, please try again"); } } catch (InterruptedException e) { Thread.currentThread().interrupt(); throw new RuntimeException("Lock interrupted", e); } } }
-
控制器(
OrderController.java
):package com.example.demo.controller; import com.example.demo.entity.Order; import com.example.demo.service.OrderService; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; @RestController @Tag(name = "订单管理", description = "订单创建与防重复提交") public class OrderController { @Autowired private OrderService orderService; @Operation(summary = "生成幂等性 Token") @GetMapping("/orders/token") public String generateToken() { return orderService.generateToken(); } @Operation(summary = "创建订单") @PostMapping("/orders") public String createOrder(@RequestBody Order order, @RequestHeader("X-Idempotency-Token") String token, @RequestParam Long userId) { orderService.createOrder(order, token, userId); return "Order created successfully"; } }
-
前端防重复提交(示例 HTML/JS):
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>订单提交</title> <script> // 防抖函数 function debounce(fn, delay) { let timer; return function (...args) { clearTimeout(timer); timer = setTimeout(() => fn.apply(this, args), delay); }; } async function createOrder() { const button = document.getElementById('submitBtn'); button.disabled = true; // 禁用按钮 try { // 获取 Token const tokenRes = await fetch('http://localhost:8081/orders/token'); const token = await tokenRes.text(); // 提交订单 const order = { orderNo: 'ORD' + Date.now(), productId: 1, quantity: 2, details: JSON.stringify({ color: 'red' }) }; const res = await fetch('http://localhost:8081/orders?userId=1', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Idempotency-Token': token }, body: JSON.stringify(order) }); alert(await res.text()); } catch (e) { alert('Error: ' + e.message); } finally { button.disabled = false; // 恢复按钮 } } // 绑定防抖事件 document.getElementById('submitBtn').addEventListener('click', debounce(createOrder, 1000)); </script> </head> <body> <button id="submitBtn">提交订单</button> </body> </html>
-
AOP 切面(
OrderMonitoringAspect.java
):package com.example.demo.aspect; import org.aspectj.lang.annotation.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.stereotype.Component; @Aspect @Component public class OrderMonitoringAspect { private static final Logger logger = LoggerFactory.getLogger(OrderMonitoringAspect.class); @Pointcut("execution(* com.example.demo.service.OrderService.createOrder(..))") public void orderMethods() {} @Before("orderMethods()") public void logMethodEntry() { logger.info("Entering order creation"); } @AfterThrowing(pointcut = "orderMethods()", throwing = "ex") public void logException(Exception ex) { logger.error("Order creation error: {}", ex.getMessage()); } }
-
ActiveMQ 消费者(
OrderLogListener.java
):package com.example.demo.service; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.jms.annotation.JmsListener; import org.springframework.stereotype.Component; @Component public class OrderLogListener { private static final Logger logger = LoggerFactory.getLogger(OrderLogListener.class); @JmsListener(destination = "order-log-queue") public void logOrder(String message) { logger.info("Order log: {}", message); } }
-
Spring Security 配置(
SecurityConfig.java
):package com.example.demo.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.User; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.provisioning.InMemoryUserDetailsManager; import org.springframework.security.web.SecurityFilterChain; @Configuration public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(auth -> auth .requestMatchers("/orders/**").authenticated() .anyRequest().permitAll() ) .httpBasic(); return http.build(); } @Bean public UserDetailsService userDetailsService() { var user = User.withDefaultPasswordEncoder() .username("user") .password("password") .roles("USER") .build(); return new InMemoryUserDetailsManager(user); } }
-
运行并验证:
- 启动应用:
mvn spring-boot:run
。 - 生成 Token:
curl http://localhost:8081/orders/token
- 输出:
550e8400-e29b-41d4-a716-446655440000
- 输出:
- 创建订单:
curl -X POST http://localhost:8081/orders?userId=1 -H "Content-Type: application/json" -H "X-Idempotency-Token: 550e8400-e29b-41d4-a716-446655440000" -u user:password -d '{"orderNo":"ORD123","productId":1,"quantity":2,"details":"{\"color\":\"red\"}"}'
- 输出:
Order created successfully
- 输出:
- 重复提交:
- 使用相同 Token 或
orderNo
重复请求,抛出异常。
- 使用相同 Token 或
- 检查 MySQL 订单表、Redis Token 和 ActiveMQ 日志。
- 启动应用:
3.2.2 原理
- 前端防抖:限制快速点击。
- 幂等性 Token:Redis 存储,确保单次有效。
- 分布式锁:Redisson 防止并发提交。
- MySQL 唯一约束:防止重复订单插入。
- AOP:监控重复提交和异常。
3.2.3 优点
- 高效防止重复提交。
- 高并发安全。
- 集成 MySQL 8.4 JSON 功能。
3.2.4 缺点
- 分布式锁增加延迟(~10ms)。
- Redis 依赖需高可用。
- 复杂配置。
3.2.5 适用场景
- 电商订单。
- 支付系统。
- 高并发表单。
四、性能与适用性分析
4.1 性能影响
- 创建订单:~20ms(含锁和数据库操作)。
- Token 校验:~2ms(Redis)。
- 日志异步:~5ms。
4.2 性能测试
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class OrderDeduplicationTest {
@Autowired
private TestRestTemplate restTemplate;
@Test
public void testOrderCreation() {
long start = System.currentTimeMillis();
ResponseEntity<String> response = restTemplate.withBasicAuth("user", "password")
.exchange("/orders?userId=1", HttpMethod.POST,
new HttpEntity<>(new Order(), new HttpHeaders() {{
set("X-Idempotency-Token", UUID.randomUUID().toString());
}}), String.class);
System.out.println("Order creation: " + (System.currentTimeMillis() - start) + " ms");
}
}
- 结果(8 核 CPU,16GB 内存):
- 正常创建:~20ms
- 重复提交:~5ms(快速拒绝)
4.3 适用性对比
方法 | 性能 | 安全性 | 适用场景 |
---|---|---|---|
前端控制 | 高 | 低 | 简单表单 |
幂等性 Token | 高 | 中 | 电商订单 |
分布式锁 | 中 | 高 | 高并发支付 |
数据库唯一约束 | 中 | 高 | 数据库密集应用 |
五、常见问题与解决方案
-
问题1:Token 失效
- 场景:用户提交时 Token 已过期。
- 解决方案:
- 延长 TTL(
application.yml
中调整)。 - 提示用户重新获取 Token。
- 延长 TTL(
-
问题2:分布式锁超时
- 场景:高并发下锁获取失败。
- 解决方案:
lock.tryLock(15, 60, TimeUnit.SECONDS); // 延长超时
-
问题3:MySQL 死锁
- 场景:高并发插入导致死锁。
- 解决方案:
SET GLOBAL innodb_deadlock_detect = ON;
-
问题4:Redis 故障
- 场景:Redis 宕机导致 Token 校验失败。
- 解决方案:
- 降级到数据库校验。
if (redisTemplate == null) { return orderRepository.existsByOrderNo(order.getOrderNo()); }
六、实际应用案例
-
案例1:电商订单:
- 场景:双十一高并发下防止重复订单。
- 方案:幂等性 Token + 分布式锁。
- 结果:重复订单率降至 0%。
-
案例2:支付系统:
- 场景:用户重复支付。
- 方案:数据库唯一约束 + Redis 锁。
- 结果:支付成功率 99.9%。
七、未来趋势
- 云原生:
- 使用 AWS ElastiCache 替代 Redis。
- AI 优化:
- AI 预测重复提交风险。
- 无服务器:
- Lambda 处理订单提交。
八、总结
通过 前端防抖 + 幂等性 Token + Redis 分布式锁 + MySQL 唯一约束,有效防止重复提交订单。示例集成 MySQL 8.4 JSON、Spring Boot 3.2、Redisson 和 AOP,性能测试表明创建订单耗时 ~20ms,重复提交快速拒绝。未来可探索云原生和 AI 优化。