找回密码
 立即注册
首页 业界区 业界 高并发下如何防止重复提交订单?

高并发下如何防止重复提交订单?

嗳诿 8 小时前
前言

当你的用户疯狂点击提交按钮时,你的系统准备好迎接这场“连击风暴”了吗?
在电商系统的实战中,我见过太多因重复提交导致的资损事故——用户一次点击,系统却创建了多个订单,导致库存错乱、用户重复支付、客服投诉爆棚。
有些小伙伴在工作中可能遇到过这样的场景:大促期间,用户反馈“明明只点了一次,为什么扣了两次款?”
开发同学查了半天日志,发现同一个用户请求在毫秒级内真的到达了服务器两次。
今天这篇文章就跟大家聊聊高并发下防止重复提交订单,希望对你会有所帮助。
01 为什么会重复提交?

在深入解决方案前,我们必须搞清楚重复提交是如何发生的。
常见的场景有:

  • 用户无意识重复点击:网络延迟时,用户心急多次点击提交按钮
  • 前端防抖失效:前端做了防抖处理,但被绕过或配置不当
  • 网络超时重试:请求超时后,客户端或网关自动重试
  • 恶意攻击:竞争对手或黑客故意重复提交
  • 后端处理超时:第一个请求处理慢,客户端以为失败又发一次
来看一个典型的用户操作流程,以及其中可能发生重复的各个环节:
1.png

从图中可以看到,从用户点击到订单落库,几乎每个环节都可能成为重复提交的“案发现场”。
下面,我们就针对这些环节,层层布防。
02 第一道防线:前端防抖与按钮控制

这是最直观、成本最低的防护措施。
原则是:在用户交互层面尽量减少无效请求
2.1 按钮状态控制
  1. // 前端防抖实现示例(Vue + Element UI)
  2. <template>
  3.   <el-button
  4.     :loading="submitting"
  5.     :disabled="submitting"
  6.     @click="handleSubmitOrder"
  7.   >
  8.     {{ submitting ? '提交中...' : '提交订单' }}
  9.   </el-button>
  10. </template>
复制代码
2.2 请求防抖与拦截
  1. // 使用axios拦截器实现请求防抖
  2. import axios from 'axios'
  3. // 存储正在进行的请求
  4. const pendingRequests = new Map()
  5. // 生成请求key
  6. const generateReqKey = (config) => {
  7.   const { method, url, params, data } = config
  8.   return [method, url, JSON.stringify(params), JSON.stringify(data)].join('&')
  9. }
  10. // 请求拦截器
  11. axios.interceptors.request.use(config => {
  12.   const key = generateReqKey(config)
  13.   
  14.   if (pendingRequests.has(key)) {
  15.     // 请求已存在,取消当前请求
  16.     config.cancelToken = new axios.CancelToken(cancel => {
  17.       cancel(`重复请求已被拦截: ${key}`)
  18.     })
  19.   } else {
  20.     // 新请求,添加到pending中
  21.     pendingRequests.set(key, config)
  22.   }
  23.   
  24.   return config
  25. })
  26. // 响应拦截器
  27. axios.interceptors.response.use(
  28.   response => {
  29.     const key = generateReqKey(response.config)
  30.     pendingRequests.delete(key)
  31.     return response
  32.   },
  33.   error => {
  34.     if (axios.isCancel(error)) {
  35.       console.log('请求被取消:', error.message)
  36.       return Promise.reject(error)
  37.     }
  38.    
  39.     // 错误处理完成后,也要从pending中移除
  40.     if (error.config) {
  41.       const key = generateReqKey(error.config)
  42.       pendingRequests.delete(key)
  43.     }
  44.    
  45.     return Promise.reject(error)
  46.   }
  47. )
复制代码
前端防护小结

  • 优点:实现简单,能拦截大部分用户无意识的重复点击
  • 缺点:可被绕过(如直接调用API、禁用JS、使用Postman等工具)
  • 结论:前端防护是必要但不充分的措施,绝不能作为唯一防线
03 第二道防线:后端接口幂等性设计

幂等性是解决重复提交的核心理念
所谓幂等,就是同一个操作执行多次的结果与执行一次的结果相同
3.1 什么是幂等性?

对于订单提交接口:

  • 幂等:无论调用1次还是N次,都只创建一个订单
  • 非幂等:调用N次可能创建N个订单
3.2 基于Token的幂等实现

这是最常用的幂等实现方案,流程如下:

  • 客户端在提交前,先向后端申请一个唯一Token
  • 提交订单时携带此Token
  • 服务端检查Token是否已使用过
  1. // 幂等性Token服务
  2. @Service
  3. public class IdempotentTokenService {
  4.    
  5.     @Autowired
  6.     private RedisTemplate<String, String> redisTemplate;
  7.    
  8.     private static final String IDEMPOTENT_PREFIX = "idempotent:token:";
  9.     private static final long TOKEN_EXPIRE_SECONDS = 300; // Token有效期5分钟
  10.    
  11.     /**
  12.      * 生成幂等性Token
  13.      */
  14.     public String generateToken(String userId) {
  15.         String token = UUID.randomUUID().toString();
  16.         String redisKey = IDEMPOTENT_PREFIX + userId + ":" + token;
  17.         
  18.         // 存储Token,设置过期时间
  19.         redisTemplate.opsForValue().set(
  20.             redisKey,
  21.             "1",
  22.             TOKEN_EXPIRE_SECONDS,
  23.             TimeUnit.SECONDS
  24.         );
  25.         
  26.         return token;
  27.     }
  28.    
  29.     /**
  30.      * 检查并消费Token
  31.      * @return true: Token有效且消费成功; false: Token无效或已消费
  32.      */
  33.     public boolean checkAndConsumeToken(String userId, String token) {
  34.         String redisKey = IDEMPOTENT_PREFIX + userId + ":" + token;
  35.         
  36.         // 使用Lua脚本保证原子性
  37.         String luaScript = """
  38.             if redis.call('get', KEYS[1]) == '1' then
  39.                 redis.call('del', KEYS[1])
  40.                 return 1
  41.             else
  42.                 return 0
  43.             end
  44.             """;
  45.         
  46.         DefaultRedisScript<Long> redisScript = new DefaultRedisScript<>();
  47.         redisScript.setScriptText(luaScript);
  48.         redisScript.setResultType(Long.class);
  49.         
  50.         Long result = redisTemplate.execute(
  51.             redisScript,
  52.             Collections.singletonList(redisKey)
  53.         );
  54.         
  55.         return result != null && result == 1L;
  56.     }
  57. }
  58. // 使用AOP实现幂等性校验
  59. @Target(ElementType.METHOD)
  60. @Retention(RetentionPolicy.RUNTIME)
  61. public @interface Idempotent {
  62.     String key() default ""; // 幂等键,支持SpEL表达式
  63.     long expireTime() default 300; // 过期时间,秒
  64. }
  65. @Aspect
  66. @Component
  67. public class IdempotentAspect {
  68.    
  69.     @Autowired
  70.     private RedisTemplate<String, String> redisTemplate;
  71.    
  72.     @Around("@annotation(idempotent)")
  73.     public Object around(ProceedingJoinPoint joinPoint, Idempotent idempotent) throws Throwable {
  74.         // 1. 获取方法参数
  75.         Object[] args = joinPoint.getArgs();
  76.         MethodSignature signature = (MethodSignature) joinPoint.getSignature();
  77.         Method method = signature.getMethod();
  78.         
  79.         // 2. 解析幂等键(支持SpEL)
  80.         String keyExpression = idempotent.key();
  81.         String redisKey = parseKey(keyExpression, method, args);
  82.         
  83.         // 3. 尝试获取分布式锁(防止并发请求同时通过检查)
  84.         String lockKey = redisKey + ":lock";
  85.         boolean lockAcquired = false;
  86.         try {
  87.             // 尝试加锁
  88.             lockAcquired = redisTemplate.opsForValue()
  89.                 .setIfAbsent(lockKey, "1", 10, TimeUnit.SECONDS);
  90.             
  91.             if (!lockAcquired) {
  92.                 throw new BusinessException("系统繁忙,请稍后重试");
  93.             }
  94.             
  95.             // 4. 检查Token是否已使用
  96.             Boolean exists = redisTemplate.hasKey(redisKey);
  97.             if (Boolean.TRUE.equals(exists)) {
  98.                 // Token已使用,直接返回之前的处理结果(这里需要根据实际业务调整)
  99.                 throw new BusinessException("请勿重复提交订单");
  100.             }
  101.             
  102.             // 5. 执行业务逻辑
  103.             Object result = joinPoint.proceed();
  104.             
  105.             // 6. 标记Token已使用
  106.             redisTemplate.opsForValue().set(
  107.                 redisKey,
  108.                 "processed",
  109.                 idempotent.expireTime(),
  110.                 TimeUnit.SECONDS
  111.             );
  112.             
  113.             return result;
  114.             
  115.         } finally {
  116.             // 释放锁
  117.             if (lockAcquired) {
  118.                 redisTemplate.delete(lockKey);
  119.             }
  120.         }
  121.     }
  122.    
  123.     private String parseKey(String expression, Method method, Object[] args) {
  124.         // 这里实现SpEL表达式解析,获取实际的幂等键
  125.         // 例如可以从参数中提取userId+orderToken
  126.         return "parsed:key:from:expression";
  127.     }
  128. }
  129. // 在订单提交接口上使用
  130. @RestController
  131. @RequestMapping("/order")
  132. public class OrderController {
  133.    
  134.     @PostMapping("/submit")
  135.     @Idempotent(key = "#request.userId + ':' + #request.submitToken", expireTime = 300)
  136.     public ApiResponse<OrderSubmitResult> submitOrder(@RequestBody OrderSubmitRequest request) {
  137.         // 这里是真正的订单创建逻辑
  138.         OrderSubmitResult result = orderService.createOrder(request);
  139.         return ApiResponse.success(result);
  140.     }
  141. }
复制代码
3.3 基于唯一业务标识的幂等

除了Token方案,还可以利用业务的自然唯一性实现幂等:
  1. @Service
  2. public class OrderService {
  3.    
  4.     @Autowired
  5.     private OrderMapper orderMapper;
  6.    
  7.     @Transactional
  8.     public OrderSubmitResult createOrder(OrderSubmitRequest request) {
  9.         // 方法1:先查询是否存在
  10.         Order existingOrder = orderMapper.selectByUniqueKey(
  11.             request.getUserId(),
  12.             request.getProductId(),
  13.             request.getSubmitTime()
  14.         );
  15.         
  16.         if (existingOrder != null) {
  17.             // 订单已存在,直接返回
  18.             return convertToResult(existingOrder);
  19.         }
  20.         
  21.         // 方法2:利用数据库唯一约束
  22.         try {
  23.             Order newOrder = buildOrder(request);
  24.             orderMapper.insert(newOrder);
  25.             return convertToResult(newOrder);
  26.         } catch (DuplicateKeyException e) {
  27.             // 捕获唯一键冲突异常
  28.             log.warn("订单重复提交,uniqueKey={}", request.getUniqueKey());
  29.             
  30.             // 查询已创建的订单并返回
  31.             Order createdOrder = orderMapper.selectByUniqueKey(
  32.                 request.getUserId(),
  33.                 request.getProductId(),
  34.                 request.getSubmitTime()
  35.             );
  36.             
  37.             if (createdOrder == null) {
  38.                 throw new BusinessException("订单处理异常,请稍后重试");
  39.             }
  40.             
  41.             return convertToResult(createdOrder);
  42.         }
  43.     }
  44.    
  45.     // 订单表可添加唯一索引
  46.     // ALTER TABLE t_order ADD UNIQUE KEY uk_user_product_time (user_id, product_id, submit_time);
  47. }
复制代码
幂等性设计小结

  • Token方案:通用性强,适合大多数场景
  • 业务标识方案:更自然,但依赖业务的天然唯一性
  • 关键点:所有幂等性检查必须在事务开始前完成,否则可能失效
04 第三道防线:数据库层防护

数据库是数据持久化的最后一道关卡,在这里设置防护至关重要。
4.1 唯一约束与乐观锁
  1. -- 订单表设计示例
  2. CREATE TABLE `t_order` (
  3.   `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主键',
  4.   `order_no` varchar(32) NOT NULL COMMENT '订单号,业务唯一',
  5.   `user_id` bigint(20) NOT NULL COMMENT '用户ID',
  6.   `product_id` bigint(20) NOT NULL COMMENT '商品ID',
  7.   `quantity` int(11) NOT NULL COMMENT '购买数量',
  8.   `amount` decimal(10,2) NOT NULL COMMENT '订单金额',
  9.   `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '订单状态:1-待支付,2-已支付',
  10.   `submit_token` varchar(64) DEFAULT NULL COMMENT '提交Token,用于幂等',
  11.   `version` int(11) NOT NULL DEFAULT '1' COMMENT '版本号,用于乐观锁',
  12.   `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
  13.   `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  14.   PRIMARY KEY (`id`),
  15.   UNIQUE KEY `uk_order_no` (`order_no`), -- 订单号唯一
  16.   UNIQUE KEY `uk_user_submit_token` (`user_id`, `submit_token`), -- 提交Token唯一
  17.   UNIQUE KEY `uk_user_product_time` (`user_id`, `product_id`, `create_time`), -- 业务维度唯一
  18.   KEY `idx_user_id` (`user_id`),
  19.   KEY `idx_create_time` (`create_time`)
  20. ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单表';
复制代码
4.2 数据库层面的幂等实现
  1. // 使用数据库事务+唯一约束保证最终一致性
  2. @Service
  3. public class OrderServiceV2 {
  4.    
  5.     @Autowired
  6.     private OrderMapper orderMapper;
  7.    
  8.     @Autowired
  9.     private IdempotentTokenService tokenService;
  10.    
  11.     @Transactional(rollbackFor = Exception.class)
  12.     public OrderSubmitResult submitOrderWithDBProtection(OrderSubmitRequest request) {
  13.         String userId = request.getUserId();
  14.         String submitToken = request.getSubmitToken();
  15.         
  16.         // 1. 检查幂等Token(在事务外先检查一次)
  17.         if (!tokenService.checkAndConsumeToken(userId, submitToken)) {
  18.             throw new BusinessException("请勿重复提交订单");
  19.         }
  20.         
  21.         try {
  22.             // 2. 生成订单号(雪花算法等分布式ID生成器)
  23.             String orderNo = generateOrderNo();
  24.             
  25.             // 3. 创建订单对象
  26.             Order order = new Order();
  27.             order.setOrderNo(orderNo);
  28.             order.setUserId(userId);
  29.             order.setProductId(request.getProductId());
  30.             order.setQuantity(request.getQuantity());
  31.             order.setAmount(calculateAmount(request));
  32.             order.setSubmitToken(submitToken);
  33.             
  34.             // 4. 插入订单(这里依赖数据库唯一约束)
  35.             orderMapper.insert(order);
  36.             
  37.             // 5. 更新库存等后续操作...
  38.             updateProductStock(request.getProductId(), request.getQuantity());
  39.             
  40.             return new OrderSubmitResult(orderNo, "订单创建成功");
  41.             
  42.         } catch (DuplicateKeyException e) {
  43.             // 6. 处理唯一约束冲突
  44.             log.warn("订单重复提交,userId={}, token={}", userId, submitToken);
  45.             
  46.             // 查询已创建的订单
  47.             Order existingOrder = orderMapper.selectBySubmitToken(userId, submitToken);
  48.             if (existingOrder != null) {
  49.                 return new OrderSubmitResult(
  50.                     existingOrder.getOrderNo(),
  51.                     "订单已创建成功,请勿重复提交"
  52.                 );
  53.             }
  54.             
  55.             // 理论上不会走到这里,除非有极端情况
  56.             throw new BusinessException("订单处理异常,请稍后重试");
  57.         }
  58.     }
  59. }
复制代码
05 第四道防线:分布式锁

在分布式环境下,多个实例可能同时处理同一个请求,需要分布式锁来保证只有一个实例执行核心逻辑。
5.1 基于Redis的分布式锁
  1. @Component
  2. public class DistributedLockService {
  3.    
  4.     @Autowired
  5.     private RedissonClient redissonClient;
  6.    
  7.     /**
  8.      * 尝试获取分布式锁
  9.      * @param lockKey 锁的key
  10.      * @param waitTime 等待时间(毫秒)
  11.      * @param leaseTime 持有时间(毫秒)
  12.      * @return 锁对象,获取失败返回null
  13.      */
  14.     public RLock tryLock(String lockKey, long waitTime, long leaseTime) {
  15.         RLock lock = redissonClient.getLock(lockKey);
  16.         try {
  17.             boolean acquired = lock.tryLock(waitTime, leaseTime, TimeUnit.MILLISECONDS);
  18.             return acquired ? lock : null;
  19.         } catch (InterruptedException e) {
  20.             Thread.currentThread().interrupt();
  21.             return null;
  22.         }
  23.     }
  24.    
  25.     /**
  26.      * 订单提交分布式锁
  27.      */
  28.     public RLock lockForOrderSubmit(String userId, String submitToken) {
  29.         String lockKey = String.format("order:submit:lock:%s:%s", userId, submitToken);
  30.         return tryLock(lockKey, 100, 5000); // 等待100ms,锁持有5秒
  31.     }
  32. }
  33. // 在订单服务中使用分布式锁
  34. @Service
  35. public class OrderServiceV3 {
  36.    
  37.     @Autowired
  38.     private DistributedLockService lockService;
  39.    
  40.     @Autowired
  41.     private OrderMapper orderMapper;
  42.    
  43.     public OrderSubmitResult submitOrderWithDistributedLock(OrderSubmitRequest request) {
  44.         String userId = request.getUserId();
  45.         String submitToken = request.getSubmitToken();
  46.         
  47.         // 1. 获取分布式锁
  48.         RLock lock = lockService.lockForOrderSubmit(userId, submitToken);
  49.         if (lock == null) {
  50.             throw new BusinessException("系统繁忙,请稍后重试");
  51.         }
  52.         
  53.         try {
  54.             // 2. 检查是否已处理
  55.             Order existingOrder = orderMapper.selectBySubmitToken(userId, submitToken);
  56.             if (existingOrder != null) {
  57.                 return new OrderSubmitResult(
  58.                     existingOrder.getOrderNo(),
  59.                     "订单已创建成功,请勿重复提交"
  60.                 );
  61.             }
  62.             
  63.             // 3. 执行业务逻辑
  64.             return doCreateOrder(request);
  65.             
  66.         } finally {
  67.             // 4. 释放锁
  68.             if (lock.isHeldByCurrentThread()) {
  69.                 lock.unlock();
  70.             }
  71.         }
  72.     }
  73.    
  74.     private OrderSubmitResult doCreateOrder(OrderSubmitRequest request) {
  75.         // 实际的订单创建逻辑
  76.         // 这里已经保证了同一时刻只有一个线程在处理同一个提交请求
  77.         // ...
  78.     }
  79. }
复制代码
5.2 分布式锁的注意事项

使用分布式锁时要注意:

  • 锁粒度:不要太粗(影响性能)也不要太细(增加复杂度)
  • 锁超时:必须设置合理的超时时间,防止死锁
  • 锁续期:对于长时间操作,需要实现锁续期机制
  • 可重入性:同一个线程可以重复获取锁
  • 容错性:Redis集群故障时要有降级方案
06 第五道防线:异步处理与消息队列

对于高并发场景,可以采用异步处理模式,将同步请求转为异步任务。
2.png

实现代码示例:
  1. // 异步订单处理实现
  2. @Component
  3. public class AsyncOrderService {
  4.    
  5.     @Autowired
  6.     private RocketMQTemplate rocketMQTemplate;
  7.    
  8.     @Autowired
  9.     private RedisTemplate<String, String> redisTemplate;
  10.    
  11.     /**
  12.      * 异步提交订单
  13.      */
  14.     public AsyncSubmitResult asyncSubmitOrder(OrderSubmitRequest request) {
  15.         // 1. 生成唯一请求ID
  16.         String requestId = generateRequestId(request.getUserId());
  17.         
  18.         // 2. 快速验证(库存、用户状态等)
  19.         quickValidate(request);
  20.         
  21.         // 3. 将请求ID与用户关联(用于查询结果)
  22.         String pendingKey = "order:pending:" + request.getUserId() + ":" + requestId;
  23.         redisTemplate.opsForValue().set(pendingKey, "processing", 10, TimeUnit.MINUTES);
  24.         
  25.         // 4. 发送到消息队列
  26.         OrderMessage message = new OrderMessage();
  27.         message.setRequestId(requestId);
  28.         message.setRequest(request);
  29.         message.setTimestamp(System.currentTimeMillis());
  30.         
  31.         rocketMQTemplate.asyncSend(
  32.             "ORDER_SUBMIT_TOPIC",
  33.             message,
  34.             new SendCallback() {
  35.                 @Override
  36.                 public void onSuccess(SendResult sendResult) {
  37.                     log.info("订单消息发送成功: {}", requestId);
  38.                 }
  39.                
  40.                 @Override
  41.                 public void onException(Throwable throwable) {
  42.                     log.error("订单消息发送失败: {}", requestId, throwable);
  43.                     // 发送失败,更新状态
  44.                     redisTemplate.opsForValue().set(
  45.                         pendingKey,
  46.                         "failed",
  47.                         5,
  48.                         TimeUnit.MINUTES
  49.                     );
  50.                 }
  51.             }
  52.         );
  53.         
  54.         // 5. 立即返回,告知用户处理中
  55.         return new AsyncSubmitResult(requestId, "订单提交成功,正在处理中");
  56.     }
  57. }
  58. // 消息消费者
  59. @Component
  60. @RocketMQMessageListener(
  61.     topic = "ORDER_SUBMIT_TOPIC",
  62.     consumerGroup = "order-submit-consumer-group"
  63. )
  64. public class OrderSubmitConsumer implements RocketMQListener<OrderMessage> {
  65.    
  66.     @Autowired
  67.     private OrderMapper orderMapper;
  68.    
  69.     @Override
  70.     public void onMessage(OrderMessage message) {
  71.         String requestId = message.getRequestId();
  72.         OrderSubmitRequest request = message.getRequest();
  73.         
  74.         // 1. 幂等检查(基于requestId)
  75.         Order existing = orderMapper.selectByRequestId(requestId);
  76.         if (existing != null) {
  77.             log.info("订单已处理,跳过: {}", requestId);
  78.             return;
  79.         }
  80.         
  81.         // 2. 创建订单
  82.         Order order = createOrder(request, requestId);
  83.         
  84.         try {
  85.             orderMapper.insert(order);
  86.             log.info("订单创建成功: {}", order.getOrderNo());
  87.             
  88.             // 3. 更新处理状态
  89.             updateProcessingStatus(request.getUserId(), requestId, "success", order.getOrderNo());
  90.             
  91.         } catch (DuplicateKeyException e) {
  92.             log.warn("订单重复,requestId={}", requestId);
  93.             // 查询已创建的订单
  94.             Order created = orderMapper.selectByRequestId(requestId);
  95.             if (created != null) {
  96.                 updateProcessingStatus(request.getUserId(), requestId, "success", created.getOrderNo());
  97.             }
  98.         }
  99.     }
  100. }
复制代码
07 综合方案:多层次联合防护

在实际生产环境中,我们通常会采用多层次、立体化的防护策略。
以下是一个完整的综合方案流程图:
3.png

这个多层次方案中,每一层都有其特定作用:

  • 前端层:用户体验优化,拦截大部分无意识重复
  • 网关层:安全防护,防刷、限流
  • 业务层:核心幂等逻辑,分布式锁保证并发安全
  • 数据层:最终保障,唯一约束防止数据不一致
  • 异步层:削峰填谷,提升系统吞吐量
08 实战:不同场景下的方案选择

不同的业务场景需要不同的防护策略,这里给出一些实践建议:
8.1 普通电商订单
  1. // 普通电商订单推荐方案
  2. @Service  
  3. public class StandardOrderService {
  4.    
  5.     // 综合使用:前端防抖 + Token幂等 + 数据库唯一约束
  6.     public OrderSubmitResult submitStandardOrder(OrderSubmitRequest request) {
  7.         // 1. 参数校验
  8.         validateRequest(request);
  9.         
  10.         // 2. 幂等Token检查(Redis)
  11.         if (!idempotentCheck(request.getUserId(), request.getSubmitToken())) {
  12.             return getExistingOrderResult(request.getUserId(), request.getSubmitToken());
  13.         }
  14.         
  15.         // 3. 分布式锁(防并发)
  16.         RLock lock = acquireOrderLock(request.getUserId(), request.getProductId());
  17.         try {
  18.             // 4. 库存检查等业务校验
  19.             checkInventory(request.getProductId(), request.getQuantity());
  20.             
  21.             // 5. 创建订单(依赖数据库唯一约束)
  22.             return createOrderInTransaction(request);
  23.         } finally {
  24.             lock.unlock();
  25.         }
  26.     }
  27. }
复制代码
8.2 秒杀订单
  1. // 秒杀订单需要更极致的优化
  2. @Service
  3. public class FlashSaleOrderService {
  4.    
  5.     // 秒杀方案:异步处理 + 库存预扣 + 最终一致性
  6.     public FlashSaleSubmitResult submitFlashSaleOrder(FlashSaleRequest request) {
  7.         // 1. 验证用户资格和活动状态(缓存中检查)
  8.         if (!checkUserQualification(request.getUserId(), request.getActivityId())) {
  9.             throw new BusinessException("您不具备参与资格");
  10.         }
  11.         
  12.         // 2. 预扣库存(Redis原子操作)
  13.         boolean stockDeducted = preDeductStock(
  14.             request.getActivityId(),
  15.             request.getProductId(),
  16.             request.getUserId()
  17.         );
  18.         
  19.         if (!stockDeducted) {
  20.             throw new BusinessException("库存不足");
  21.         }
  22.         
  23.         // 3. 生成唯一请求ID
  24.         String requestId = generateRequestId(request.getUserId(), request.getActivityId());
  25.         
  26.         // 4. 发送到消息队列(快速返回)
  27.         sendToMQ(request, requestId);
  28.         
  29.         // 5. 立即返回
  30.         return new FlashSaleSubmitResult(requestId, "秒杀请求已接受,处理中");
  31.     }
  32.    
  33.     // 消费者异步创建订单
  34.     @Transactional
  35.     public void processFlashSaleOrder(FlashSaleRequest request, String requestId) {
  36.         // 这里只需要处理真正的订单创建
  37.         // 因为库存已在Redis中预扣,只需保证最终一致性
  38.         try {
  39.             createOrder(request, requestId);
  40.             // 同步库存到数据库
  41.             syncStockToDB(request.getProductId(), request.getActivityId());
  42.         } catch (Exception e) {
  43.             // 失败时回滚Redis库存
  44.             rollbackStockInRedis(request.getActivityId(), request.getProductId(), request.getUserId());
  45.             throw e;
  46.         }
  47.     }
  48. }
复制代码
10 总结

防止重复提交订单是一个系统工程,需要从前到后、多层次的防护。
让我们回顾一下关键点:

  • 前端防护是体验,不是保障:按钮防抖、请求拦截能改善用户体验,但不能作为唯一防线。
  • 幂等性是核心理念:无论是Token方案还是业务唯一标识,都要保证同一操作执行多次的结果一致。
  • 分布式锁解决并发问题:在分布式环境下,防止多个实例同时处理同一请求。
  • 数据库是最后防线:唯一约束、乐观锁等机制能在应用层防护失效时保证数据一致性。
  • 异步处理提升吞吐:对于高并发场景,将同步请求转为异步处理,提高系统整体吞吐量。
  • 监控告警必不可少:没有监控的系统就像没有仪表的飞机,无法发现问题和优化性能。
在实际架构设计中,我通常建议采用 "前端防抖 + 网关限流 + Token幂等 + 分布式锁 + 数据库唯一约束" 的综合方案,对于秒杀等极致场景再加入异步处理。
有些小伙伴可能会觉得这些防护措施太复杂,影响开发效率。
但请记住:预防的成本远低于修复的成本
一次重复提交导致的资损事故,可能就需要整个团队加班数周来修复数据和安抚用户。
技术方案没有银弹,只有最适合业务场景的平衡。
最后说一句(求关注,别白嫖我)

如果这篇文章对您有所帮助,或者有所启发的话,帮忙关注一下我的同名公众号:苏三说技术,您的支持是我坚持写作最大的动力。
求一键三连:点赞、转发、在看。
关注公众号:【苏三说技术】,在公众号中回复:进大厂,可以免费获取我最近整理的10万字的面试宝典,好多小伙伴靠这个宝典拿到了多家大厂的offer。
更多项目实战在我的技术网站:http://www.susan.net.cn/project

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册