Yihui’s Blog

如何避免用户重复下单,多次未支付占用库存?

日期:2026-07-11
标签:#面试 #八股 #后端 #订单 #库存 #场景题

一句话答案

使用业务幂等键和唯一约束阻止重复请求,并通过“同用户同商品仅允许一个有效未支付订单”、短时库存预占和超时关单回补控制库存占用。

面试口语版

前端按钮防重复只能改善体验,服务端仍要做幂等。客户端生成 requestId,服务端以用户、商品、活动和 requestId 建唯一约束;对于业务意义上的重复订单,再限制同一用户同一商品只能存在一笔有效待支付订单,重复请求直接返回原订单。库存预占设置合理过期时间,延迟队列触发关单,状态条件更新成功后再释放库存。对恶意占库存还要加用户购买频率限制、风险评分和黑名单。

原理拆解

  • 请求幂等:同一个 requestId 只能创建一次。
  • 业务去重:userId + skuId + activityId + activeStatus 表达同一购买意图。
  • 库存状态:可用库存 → 预占库存 → 已售库存,取消时预占回退。
  • 超时关单:延迟消息触发,数据库扫描作为兜底。
  • 风控限额:限制同用户/IP/设备的未支付订单数和下单频率。

关键细节

  • MySQL 难以直接对“部分状态”建唯一索引时,可维护 active 标识或单独的购买资格表。
  • 关单与支付并发必须通过订单状态机和条件更新处理。
  • MQ 可能重复投递,释放库存应带业务流水并保证幂等,不能重复回补。
  • 不要无限延长待支付时间,热门库存应使用更短有效期。

面试官追问

  1. 用户确实想买两件怎么办?
  2. 延迟消息丢失如何关单?
  3. 订单创建成功但库存预占失败怎么办?
  4. 如何防止库存重复回补?

面试官追问参考答案

1. 用户确实想买两件怎么办?

把购买数量作为同一订单明细,而不是让用户并发创建两笔未支付订单;服务端校验活动限购和单次最大数量后一次预占。若业务允许多次购买,可把去重维度加入客户端 requestId,并单独限制用户未支付订单总数和占用库存量。

2. 延迟消息丢失如何关单?

延迟消息只是触发器,数据库中的 expireTime 才是事实。消费者幂等关单,同时定时任务按到期时间和状态扫描漏单,可用时间轮或分片索引降低扫描成本;支付前也应再次检查订单是否已过期。

3. 订单创建成功但库存预占失败怎么办?

将订单保持创建中或直接标记失败/关闭,不能暴露为可支付待支付订单。更常见是先成功预占库存再创建订单,若创建失败按业务流水补偿库存;跨系统步骤用状态机、可靠事件和定时对账最终收敛。

4. 如何防止库存重复回补?

建立唯一库存流水,例如 orderId + operationType,数据库插入流水和库存增加在同一事务中,唯一键阻止重复执行。Redis 回补也用 Lua 检查补偿标记后再增加,数据库库存作为最终对账依据。

学习清单

  • 区分请求幂等和业务去重。
  • 能说明预占、确认和释放库存的状态流转。
Maintained by · YihuiEdit on GitHub

Keep reading

View all posts