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

简介: 商城订单付了钱库存没减、甚至同一个商品被卖超了?根因十有八九是支付回调没做幂等。本文从订单状态机、库存扣减日志、支付回调重试机制三层,完整复盘一次大促超卖事故的排查与修复,附可复用的幂等代码和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分钟就能定位到方向。

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

相关文章
|
22小时前
|
Java 关系型数据库 MySQL
物联网平台数据表只涨不缩?Spring Boot 遥测数据保留策略:分批删除 + 索引优化 + 真实压测数据
本文以真实设备接入平台为例,详解时序数据治理三大核心:分批删除防锁表、复合索引+限量查询保响应、压测聚焦P99长尾。从100台设备日增172万条数据的现实压力出发,手把手拆解MySQL下高效存查清的落地实践与避坑指南。
|
1天前
|
SQL 应用服务中间件 数据库
网站半夜突然开始大量502:Nginx超时、连接池耗尽与慢查询的全链路排查
网站半夜突然大量502,根因不在网关而在应用响应不过来。本文从Nginx错误日志、应用连接池状态、数据库慢查询三层,完整复盘一次40分钟定位的真实事故,附可复用的排查命令和5条踩坑经验。
|
19小时前
|
NoSQL 小程序 测试技术
门店预约高峰期约满?容量预估与排队机制的一次压测实践
门店预约高峰期约满、同一个时段被约两次?根因十有八九是容量预估不准+并发预约没加锁。本文从预约记录分布、并发请求日志、数据库行锁三层,完整复盘一次门店预约超卖事故的压测与修复,附可复用的行锁代码和5条踩坑经验。
|
1天前
|
缓存 人工智能 安全
阿里云国际站代理商:2026云栖大会 MaaS&Agent 论坛讲了什么?要点解读
2026云栖大会发布千问AI平台重大升级:强化MaaS服务能力,推出Agent Studio与ACS安全沙箱,支持动态模型路由、上下文缓存、MicroVM级隔离及MCP标准协议,助力企业级智能体规模化落地。
|
3天前
建议大家放弃qoder
用户吐槽Qoder积分机制愈发苛刻:早期2000积分可享1倍速,如今6000积分仅0.5倍速,体验严重倒退。自国际版起长期使用,却越用越差,呼吁大家放弃该平台。
182 0
|
3天前
|
人工智能 安全 开发者
Jev 发布不到一周,为什么它这么快进入 Agent 工程?
Jev 是专为 Agent 设计的轻量级决策模型,不生成文本,专注快速输出 Choice/Score/Boolean。它被 Vercel AI Gateway、LangChain 等迅速集成,用于路由、流程控制、安全守卫和评估等高频判断场景,显著降本增效,推动 Agent 架构向“分层智能”演进。
Jev 发布不到一周,为什么它这么快进入 Agent 工程?
|
2天前
|
存储 人工智能 安全
千问办公官网入口网页版登录:qwenwork.cn 注册送2000积分、登录送100积分
阿里云千问办公(QwenWork)是AI智能工作平台,支持一句话生成PPT、表格、网页、视频及数据分析。个人免费版送2000积分+每日登录赠100分,含1GB存储、5个发布页;企业版198元/席/月起。阿里千问办公QwenWork官网:https://t.aliyun.com/U/0VCTGt 阿里AI工作平台,一句话完成数据分析、PPT 生成、视频剪辑、网页搭建等复杂任务
|
2天前
|
弹性计算 人工智能 数据库
阿里云最新特惠云服务器活动及价格及新老用户便宜购买指南参考
针对新手选购阿里云服务器时面对繁杂促销规则、易多花冤枉钱的痛点,本文系统梳理全链路省钱实操方法:从140余款全品类云产品免费试用零成本验证需求,到38元/年起轻量抢购款、99/199元续费同价长效特惠款,再到各系列实例阶梯折扣、多产品组合购专属套餐,最后覆盖全类型优惠券折上折技巧,形成完整采购省钱链路,帮助不同用户精准压缩上云成本。
|
1天前
|
存储 人工智能 安全
阿里云国际代理商:2026 年云栖大会 “无法计算的价值主论坛” 讲了什么?AI 共生共治共善解读
2026云栖大会以“智以致用”为主题,聚焦“为了无法计算的价值”,提出AI应超越算力堆叠,迈向产业赋能与人文共融。大会发布真武V900芯片、CPFS存储、AgentCore平台等全栈技术,并倡导“共生、共治、共善”三维范式,推动AI向绿色、安全、普惠演进。(239字)
|
19小时前
|
关系型数据库 MySQL 测试技术
海外版外卖平台结算:云主机规格怎么选
海外外卖结算系统需分三类负载(写库、读库、异步导出)合理选型云主机:下单用API+主库,查询走缓存,周结导出应异步至OSS。避免周五导出拖慢下单,推荐独立worker、低峰cron、压测叠加场景,并做好磁盘、连接池与生命周期管理。