在日常开发中,订单重复提交是一个常见但危害极大的问题。它不仅影响用户体验,还可能导致资金损失、库存异常等严重后果。本文将深入探讨订单重复提交的原因,并介绍多种有效的防范方案。
订单重复问题通常源于以下几个方面:
前端防护不能完全防止重复提交,但能有效提升用户体验。
// 按钮防抖处理
let submitting = false;
function submitOrder() {
if (submitting) {
return;
}
submitting = true;
// 显示加载中状态
showLoading();
// 提交订单请求
api.submitOrder(data)
.then(response => {
// 处理成功
})
.catch(error => {
// 处理错误
})
.finally(() => {
submitting = false;
hideLoading();
});
}
// 或者使用更优雅的防抖函数
const debounceSubmit = debounce(function(params) {
// 提交逻辑
}, 1000);
这是最常用的防重复提交方案,核心思路是每次页面加载时生成一个唯一令牌。
// 生成令牌并存储在session中
String token = UUID.randomUUID().toString();
request.getSession().setAttribute("ORDER_TOKEN", token);
// 在表单中添加隐藏字段
<input type="hidden" name="orderToken" value="${orderToken}">
// 服务器端验证令牌
public boolean checkToken(HttpServletRequest request) {
String serverToken = (String) request.getSession().getAttribute("ORDER_TOKEN");
String clientToken = request.getParameter("orderToken");
if (serverToken == null || !serverToken.equals(clientToken)) {
return false;
}
// 验证成功后立即移除令牌,防止重复使用
request.getSession().removeAttribute("ORDER_TOKEN");
return true;
}
幂等性是分布式系统中的重要概念,保证同一请求多次执行与一次执行效果相同。
CREATE TABLE orders (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
order_no VARCHAR(32) NOT NULL UNIQUE, -- 订单号唯一索引
user_id BIGINT NOT NULL,
amount DECIMAL(10,2) NOT NULL,
-- 其他字段...
);
@Service
public class OrderService {
@Transactional
public Order createOrder(OrderCreateRequest request) {
try {
// 生成唯一订单号(时间戳+随机数+用户ID等)
String orderNo = generateOrderNo(request.getUserId());
Order order = new Order();
order.setOrderNo(orderNo);
// 设置其他属性...
orderMapper.insert(order);
return order;
} catch (DuplicateKeyException e) {
// 捕获唯一键冲突异常
log.warn("订单重复提交: {}", request);
return orderMapper.selectByOrderNo(orderNo);
}
}
}
public Order createOrderWithLock(OrderCreateRequest request) {
String lockKey = "order:lock:" + request.getUserId();
String requestId = UUID.randomUUID().toString();
try {
// 尝试获取分布式锁
boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, requestId, 30, TimeUnit.SECONDS);
if (!locked) {
throw new BusinessException("请勿重复提交订单");
}
// 处理订单创建逻辑
return createOrder(request);
} finally {
// 释放锁
if (requestId.equals(redisTemplate.opsForValue().get(lockKey))) {
redisTemplate.delete(lockKey);
}
}
}
通过订单状态机防止重复操作:
public enum OrderStatus {
INIT(0), // 初始状态
PAID(1), // 已支付
COMPLETED(2), // 已完成
CANCELED(-1); // 已取消
private final int code;
// 构造方法、getter等
}
public Order payOrder(String orderNo, BigDecimal amount) {
Order order = orderMapper.selectByOrderNo(orderNo);
if (order == null) {
throw new BusinessException("订单不存在");
}
// 检查订单状态
if (order.getStatus() != OrderStatus.INIT.getCode()) {
throw new BusinessException("订单已处理,请勿重复操作");
}
// 更新订单状态
int rows = orderMapper.updateStatus(orderNo, OrderStatus.INIT.getCode(), OrderStatus.PAID.getCode());
if (rows == 0) {
throw new BusinessException("订单状态已变更,请刷新后重试");
}
// 其他支付逻辑...
}
对请求内容进行哈希,在一定时间内拒绝相同哈希值的请求:
public class RequestDeduplication {
@Autowired
private RedisTemplate<String, String> redisTemplate;
public boolean isDuplicate(String dedupKey, long expireTime) {
Boolean result = redisTemplate.opsForValue().setIfAbsent(dedupKey, "1", expireTime, TimeUnit.SECONDS);
return result == null || !result;
}
public String generateDedupKey(HttpServletRequest request, String userId) {
// 生成请求指纹:用户ID+接口路径+参数哈希
String requestParams = getRequestParams(request);
String requestPath = request.getRequestURI();
String content = userId + "_" + requestPath + "_" + requestParams;
return "dedup:" + DigestUtils.md5DigestAsHex(content.getBytes());
}
}
方案 | 适用场景 | 优点 | 缺点 |
---|---|---|---|
前端防护 | 所有Web应用 | 实现简单,提升用户体验 | 不可靠,可绕过 |
令牌机制 | Web表单提交 | 简单有效,用户体验好 | 不适用于API接口 |
数据库唯一索引 | 所有应用 | 绝对可靠,实现简单 | 索引性能开销 |
分布式锁 | 分布式系统 | 保证强一致性 | 实现复杂,性能开销 |
状态机校验 | 有状态业务流程 | 业务逻辑合理 | 需要设计状态机 |
请求指纹 | API接口调用 | 防止参数相同的重复请求 | 计算和存储开销 |
防止订单重复提交需要根据具体业务场景选择合适的方案组合。对于简单的Web应用,令牌机制+数据库唯一索引可能就足够了;对于复杂的分布式系统,可能需要分布式锁+幂等性设计+状态机校验的组合方案。
最重要的是,要将防重复提交作为系统设计的一部分,而不是事后补救措施。通过合理的技术方案和业务设计,可以有效地避免订单重复问题,保障系统的稳定性和数据的准确性。
记住:前端防护只是用户体验优化,真正的防重复必须在服务端实现!