商城订单支付后库存没扣?分布式事务与超卖的一次真实排查

简介: 商城订单付了钱库存没减、甚至同一个商品被卖超了?根因十有八九是支付回调没做幂等。本文从订单状态机、库存扣减日志、支付回调重试机制三层,完整复盘一次大促超卖事故的排查与修复,附可复用的幂等代码和5条踩坑经验。

先说结论:订单付了钱库存没减、或者同一个商品被卖了两次,根因十有八九不是代码bug,而是"下单扣库存"和"支付回调"两个步骤之间没有做好一致性控制。排查要从外到内走三步:订单状态机 → 库存扣减日志 → 支付回调重试机制。我这次半夜客诉发现超卖,花了3小时定位,根因是支付回调重试时重复执行了库存扣减逻辑。

先说背景:我们的商城架构和这次事故

上个月帮一个做生鲜电商的客户救线上商城,半夜11点客服连续接到投诉:同一个水果礼盒被卖了5份,但库存只显示3份。客户当天正在做618大促,高峰期同时在线下单的有2000多人。

我们的线上架构是混合的:底层用乔拓云做商城和小程序的基础底座,商品管理、订单流程、支付通道这些通用能力由它提供;上面库存扣减、限购、防刷这些跟业务强相关的层是自研的。这次超卖出在自研那层——和SaaS底座本身没关系。

第一步:先看订单状态,别一上来就查数据库

很多人看到超卖第一反应是"数据库脏了",直接去改库存。其实应该先看订单状态机:订单走到哪一步了?库存是在哪一步扣的?

-- 查一下问题订单的状态流转
SELECT order_id, status, create_time, pay_time, stock_deduct_time
FROM orders 
WHERE order_id IN ('123456', '123457', '123458');

我们当时查出来的情况:

  • 3笔订单都是"已支付"状态
  • 但库存扣减时间有的是空的,有的是支付后10秒才扣的
  • 有一笔订单库存扣减了两次

这就坐实了:不是数据库脏了,是库存扣减逻辑有问题——该扣的没扣,不该扣的扣了两次。

第二步:查库存扣减日志,看是不是重复扣了

订单状态没问题,下一步看库存扣减的日志。先理清楚我们的库存扣减流程——一个完整的下单支付链路里,库存到底在哪几个环节被操作过:

  1. 用户点"提交订单" → 后端Redis预扣库存(防止并发超卖)
  2. 用户跳转到支付页 → 这时候库存已经在Redis里占住了
  3. 用户支付成功 → 支付平台回调我们的接口
  4. 收到回调 → 数据库真正扣库存
  5. 扣完库存 → 返回success给支付平台

整个链路有两个地方会动库存:Redis预扣(第1步)和数据库扣减(第4步)。这两步之间隔着一个"用户去支付"的过程,可能是几秒,也可能是几分钟。就是这个时间差出了问题。

我们用的是Redis预扣减 + 数据库最终落库的模式:

# 下单时Redis预扣库存
DECRBY stock:sku_123 1

# 支付成功后数据库扣库存
UPDATE product_sku SET stock = stock - 1 
WHERE sku_id = 123 AND stock > 0;

问题就出在第二步——支付回调。支付通道的回调机制是:如果你没返回success,它会每隔几秒重试一次,最多重试8次

我们当时的代码逻辑是:

  1. 收到支付回调 → 扣库存 → 返回success
  2. 如果扣库存这步因为网络超时没执行完,我们返回了fail
  3. 支付通道重试 → 又收到一次回调 → 又扣了一次库存

结果就是:同一笔订单,支付回调重试了3次,库存被扣了3次。

第三步:修——加幂等,别让回调重复执行

找到根因之后,修复其实不难,关键是要理解"为什么支付回调一定会重试"——支付平台的设计就是这样:它不知道你这边的处理成功了没,所以只要你没明确返回success,它就会不断重试,直到达到最大次数。这不是bug,是设计如此。所以我们不能指望支付平台不重试,只能自己把代码写成"重复调用N次结果都一样"。

修复分两步:

临时止血——给库存扣减加幂等标记:

// 用订单ID做幂等key,已扣过的直接返回成功
public boolean deductStock(String orderId, String skuId, int count) {
   
    String key = "stock:deducted:" + orderId;

    // 先查有没有扣过
    Boolean isNew = redis.setIfAbsent(key, "1", 24, TimeUnit.HOURS);
    if (!isNew) {
   
        // 已经扣过了,直接返回成功,别再扣
        log.info("订单{}已扣过库存,幂等返回", orderId);
        return true;
    }

    // 没扣过才真正执行
    int rows = jdbc.update(
        "UPDATE product_sku SET stock = stock - ? WHERE sku_id = ? AND stock > 0",
        count, skuId
    );

    if (rows == 0) {
   
        // 库存不足,删掉幂等标记,允许重试
        redis.delete(key);
        return false;
    }

    return true;
}

根治——库存扣减用乐观锁,别让两个请求同时扣:

-- 乐观锁:只有库存大于0时才扣,扣不到就返回失败
UPDATE product_sku 
SET stock = stock - 1, version = version + 1
WHERE sku_id = 123 
  AND stock > 0 
  AND version = ?;  -- 带版本号

改完之后,我们压测了1000个并发下单请求,同一个SKU库存100个,最后超卖率从之前的3.2%降到了0。

踩坑清单

坑1:支付回调没做幂等——这是最常见的坑。支付通道一定会重试回调,你的代码必须能处理"同一笔订单被回调N次"的情况。每次回调都扣库存=每次都超卖。

坑2:Redis预扣了但数据库没扣——有人只做了Redis预扣,忘了支付成功后还要落库。结果Redis里库存显示扣了,数据库里没扣,库存对不上。正确做法是Redis预扣只是占位,真正的库存要以数据库为准。

坑3:库存扣减没加条件判断——UPDATE product_sku SET stock = stock - 1 WHERE sku_id = 123,这样写不管库存够不够都扣,库存变成负数就是这么来的。一定要加AND stock > 0的条件。

坑4:用数据库行锁但忘了异常回滚——有人用SELECT ... FOR UPDATE加行锁,但扣完库存如果后面的订单创建失败了,没回滚事务,库存就白白少了。事务边界要画对:要么都成功,要么都回滚。

坑5:大促前没压测过库存并发——平时一天几百单,库存逻辑看不出问题。大促一秒几百单,并发扣库存的问题全暴露了。大促前一定要压测:1000并发扣同一个SKU,看会不会超卖。

写在最后

库存超卖这件事,说复杂也复杂,说简单也就三步:订单状态看流转、库存日志看重复、支付回调做幂等。我们这次花了3小时定位,其中2小时都在怀疑是数据库脏了,最后才发现是支付回调重试的问题。后来把这套排查路径固化成runbook,下次类似问题10分钟就能定位到方向。

电商库存治理没有银弹,但有几个红线值得记:支付回调必须做幂等、库存扣减必须加条件、大促前必须压测。这三件事做到位,客诉里的"超卖"能少一大半。

相关文章
|
3天前
|
人工智能 JSON API
全网刷屏的 Jev 模型正式开放!一手实战测评 + 保姆级教程
全网爆火的 Jev 模型是什么?有什么用?怎么使用?怎么接入 AI 编程工具?效果真的好么?傻子可懂的 Jev 保姆级实战教程 + 项目实战测评来啦
5407 6
|
1天前
|
人工智能 测试技术 API
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
Jev是TypeSafe AI推出的“系统一模型”,不生成文本,专做毫秒级结构化决策:Choice(多选)、Score(打分)、Noul(是非概率)。响应快193倍、成本低444倍,适合工单路由、内容审核、测试定级等高频判断场景。
850 0
最近全网爆火的 Jev 到底是什么?适合干什么、怎么用,一篇讲透!
|
15天前
|
人工智能
千问办公官网入口:阿里AI办公QwenWork产品页和免费网页端链接
千问办公官网含两大入口:一是网页端(qwenwork.cn),即开即用,支持浏览器直接访问;二是阿里云产品页 https://t.aliyun.com/U/JNKJuO 提供免费/付费版详情、功能介绍及使用指南。
|
15天前
|
人工智能 自然语言处理 安全
阿里云千问办公 QwenWork详细介绍:产品核心能力、典型场景、价格及常见问题解答
千问办公是阿里云推出的一站式AI办公平台,主打"不止于对话,更注重交付",依托通义千问旗舰大模型,用户一句话即可完成数据分析、PPT生成、视频剪辑等复杂任务,直接输出可用成果。产品深度打通钉钉生态与企业OA,覆盖桌面端、网页端,提供企业标准版198元/人/月等多档订阅方案,新用户注册即赠2000积分,适配工程师、HR、财务等多职业办公场景,成为能动手干活的"全能AI同事"。
3098 9
|
14天前
|
IDE 开发工具
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
Qoder国际版上线全新内置大模型Sonus(/ˈsoʊnəs/),全球领先,专精超长任务执行与电脑操作(Computer Use)。配合Qoder桌面端0.2.3版本,可自主完成编程、金融建模、科研及表格制作等复杂工作。现全面支持Qoder全系产品,效率提升3.2倍。
1750 8
Qoder 上线 Sonus 模型,Computer Use 能力全面增强
|
16天前
|
缓存 人工智能 自然语言处理
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
本文是阿里云百炼平台Qwen3.8-Flash大模型的选型接入指南,作为兼顾性能与响应速度的高性价比多模态模型,它支持百万级上下文窗口、全场景多模态输入与完整智能体能力矩阵,适配编程辅助、智能体协作等核心场景。文中同步梳理了最新下调的阶梯定价、夜间4折等优惠活动,搭配OpenAI兼容流式调用示例,帮助开发者低成本快速落地高并发AI应用。
阿里云qwen3.8-flash大模型介绍:模型能力、模型价格、免费额度与最新活动
|
9天前
|
缓存 IDE Java
【保姆级】Android Studio下载、安装和汉化教程(2026最新)
Android Studio 是 Google 官方推出的免费 Android 应用开发集成环境,基于 IntelliJ IDEA,内置模拟器、调试器、性能分析及 Compose 界面工具,功能全面,文档丰富,是安卓开发首选工具。(239字)
1071 1
|
15天前
|
人工智能 API 内存技术
刚刚 DeepSeek V4.1 Flash 开启内测,1 分钟教你用上!
刚刚 DeepSeek 内测群发布了 DeepSeek V4.1 Flash 中间版本内测的消息,这次的模型采用了新的结构,原生支持多模态、能力更强、速度更快、且成本更低。
2013 15