重复提交订单是电子商务、支付系统和在线服务中常见的难题,可能导致库存错误、财务异常或用户体验下降。重复提交通常由用户快速点击、浏览器刷新、网络重试或恶意操作引起。本文将分析重复提交订单的原因,提供多种防止重复提交的解决方案,并在 Spring Boot 3.2 中实现一个电商订单系统,集成 MySQL 8.4、Redis 分布式锁、AOP 监控和幂等性控制。本文目标是为开发者提供一份全面的中文技术指南,帮助在 2025 年的高并发场景下有效防止重复提交订单。
本文选择 前端控制 + 幂等性 Token + Redis 分布式锁 的组合方案,结合 MySQL 8.4 的 JSON 功能和 Spring Boot 生态,适合高并发电商场景。
以下是一个电商订单系统的实现,防止重复提交订单,集成 MySQL 8.4、Redis、Redisson 和 AOP。
创建 Spring Boot 项目:
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.0modelVersion>
<parent>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-parentartifactId>
<version>3.2.0version>
parent>
<groupId>com.examplegroupId>
<artifactId>order-deduplication-demoartifactId>
<version>0.0.1-SNAPSHOTversion>
<dependencies>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-webartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-jpaartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-data-redisartifactId>
dependency>
<dependency>
<groupId>mysqlgroupId>
<artifactId>mysql-connector-javaartifactId>
<version>8.0.33version>
dependency>
<dependency>
<groupId>org.redissongroupId>
<artifactId>redisson-spring-boot-starterartifactId>
<version>3.23.2version>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-activemqartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-aopartifactId>
dependency>
<dependency>
<groupId>org.springframework.bootgroupId>
<artifactId>spring-boot-starter-securityartifactId>
dependency>
dependencies>
project>
准备数据库和 Redis:
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)
);
配置 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
运行并验证:
mvn spring-boot:run
。实现订单创建接口,防止重复提交。
实体类(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
。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
orderNo
重复请求,抛出异常。@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");
}
}
方法 | 性能 | 安全性 | 适用场景 |
---|---|---|---|
前端控制 | 高 | 低 | 简单表单 |
幂等性 Token | 高 | 中 | 电商订单 |
分布式锁 | 中 | 高 | 高并发支付 |
数据库唯一约束 | 中 | 高 | 数据库密集应用 |
问题1:Token 失效
application.yml
中调整)。问题2:分布式锁超时
lock.tryLock(15, 60, TimeUnit.SECONDS); // 延长超时
问题3:MySQL 死锁
SET GLOBAL innodb_deadlock_detect = ON;
问题4:Redis 故障
if (redisTemplate == null) {
return orderRepository.existsByOrderNo(order.getOrderNo());
}
案例1:电商订单:
案例2:支付系统:
通过 前端防抖 + 幂等性 Token + Redis 分布式锁 + MySQL 唯一约束,有效防止重复提交订单。示例集成 MySQL 8.4 JSON、Spring Boot 3.2、Redisson 和 AOP,性能测试表明创建订单耗时 ~20ms,重复提交快速拒绝。未来可探索云原生和 AI 优化。