当前位置: 首页 > news >正文

防住了超卖,却输给了“少卖”?



兄弟们,大家做电商或者秒杀系统时,第一反应防什么?肯定是**“超卖”**对吧?

毕竟,库存只有 100 个,结果卖出去 101 个,不仅要赔钱,搞不好还要被老板请去喝茶。于是我们搬出了 Redis,祭出了 Lua 脚本,觉得稳如老狗。

但你有没有想过,还有一种情况比超卖更让老板心痛?那就是——“少卖”

少卖:库存明明显示扣掉了,Redis 里也没货了,但数据库里订单压根没生成!货烂在仓库里卖不出去,原本能赚的钱飞了。

今天我们就来扒一扒这个“少卖”是怎么发生的,顺便聊聊在订单支付环节,如何用状态机+乐观锁把并发问题治得服服帖帖。


库存到底什么时候扣?

在写代码之前,产品经理通常会跑过来问你一个哲学问题:“咱们是下单减库存,还是支付减库存?”

这不仅仅是技术实现的问题,更是业务体验的选择。我们先来看看这两种流派的爱恨情仇:

1. 支付减库存(Pay-to-Deduct)

  • 逻辑:用户下单随便下,库存不改。只有当用户真正付完钱那一刻,才去扣库存。
  • 优点:绝对不会产生“恶性占库存”的情况,卖出去的都是真金白银。
  • 缺点用户体验极差。想象一下,你双11抢到了手机,开开心心去付款,结果银行卡扣款时告诉你“没货了,退款吧”。用户绝对会炸毛。而且在并发高时,这会导致严重的超卖风险(因为大家都能下单)。

2. 下单减库存(Order-to-Deduct)

  • 逻辑:用户只要下单成功,库存就锁住。
  • 优点:用户体验好,只要下单成功,就一定能买到(除非他不付钱)。
  • 缺点:容易被“恶作剧”或者“竞对”恶意刷单,把库存占满但不付款,导致真正想买的人买不到。

最终方案:Redis 预扣 + 数据库实扣

在高并发/秒杀场景下,我们通常采用折中方案

  1. 下单阶段(Redis 预扣):为了抗住流量,我们在 Redis 里进行库存扣减(也就是上面的“下单减库存”逻辑)。只要 Redis 扣成功,就告诉用户“抢到了”。
  2. 支付阶段(DB 实扣):用户支付成功后,我们再异步或者同步地去扣减数据库里的真实库存。

这就引出了我们接下来的核心技术点——如何在 Redis 里安全地扣库存?


第一回合:Redis 挡在最前面(防超卖)

在秒杀场景下,直接怼数据库肯定是找死。通常我们会在 Redis 里做缓存扣减。

为了保证“查库存”和“扣库存”这两个动作中间不被别人插队,我们通常会用 Lua 脚本。这就好比你去买奶茶,店员看库存、收钱、给号这一套动作必须是一口气做完的,中间不能接电话。

Redis Lua 扣减脚本

-- KEYS[1]: 商品库存Key-- ARGV[1]: 要购买的数量localstock=tonumber(redis.call('get',KEYS[1]))localamount=tonumber(ARGV[1])ifstockandstock>=amountthen-- 库存充足redis.call('decrby',KEYS[1],amount)return1-- 成功elsereturn0-- 库存不足end

利用 Redis 单线程执行 Lua 脚本的特性,我们完美解决了原子性问题,超卖?不存在的。


第二回合:隐秘的角落——“少卖”是怎么来的?

上面那步做完,Redis 库存是扣了,接下来我们要把订单落库。为了不把数据库打挂,我们通常是异步的。

问题就出在这个异步链路里。

正常流程 vs 少卖流程

用户Redis缓存消息队列数据库正常流程1. 扣减库存 (成功)2. 发送创建订单消息3. 消费消息写入订单下单成功"少卖"事故现场1. 扣减库存 (成功 -1)此时 Redis 库存已减少2. 发送消息失败 (网络抖动/服务挂了)消息丢失!3. 没收到消息,不写入数据库没订单,但库存被扣了!用户Redis缓存消息队列数据库

结果就是:Redis 里的库存已经少了(被你扣了),但数据库里并没有生成订单。

这就像是你去买票,售票员把票撕下来给你留着(Redis库存-1),结果你付钱的时候断网了,人走了。这张票就被“锁死”在售票员手里,别人买不到,你也买不走。这就是“少卖”。

怎么解决?
除了保证 MQ 的可靠性投递(本地消息表、ACK机制),最稳妥的办法是引入**“库存回补”机制或者“对账”**。如果一定时间内订单没创建成功,要把 Redis 里的库存加回去。


第三回合:支付与关单的“生死时速”(状态机+乐观锁)

好,假设现在库存没问题,订单也生成了,状态是PENDING(待支付)。

这时候,真正的并发大坑来了。

场景模拟:
用户小明在订单快超时(比如 30 分钟)的最后一秒,点击了支付。

  1. 线程 A(支付回调):收到银行通知,用户钱付了,要把订单改成PAID
  2. 线程 B(定时任务):巡逻发现这单 30 分钟没付钱,要把它改成CLOSED(关单)并释放库存。

如果这两个线程同时执行,会发生什么?如果不加控制,可能出现:用户钱付了,订单却被关闭了。

解决方案:状态机 + 乐观锁

我们不能让订单状态随意跳转,必须按规矩办事。

1. 状态机设计(立规矩)

我们要定义好状态流转的方向,不能逆行。

下单成功
支付成功 (允许)
超时未付 (允许)
支付回调 (❌ 禁止!)
定时关单 (❌ 禁止!)
PENDING
PAID
CLOSED
2. 乐观锁实现(加版本号)

我们在更新数据库时,利用 SQL 的原子性做一个 CAS(Compare And Swap)操作。不要只是简单的update,而是要带上前置条件

Java 伪代码感受一下:

// 支付成功的处理逻辑publicbooleanpaySuccess(longorderId){// 只有当前状态是 PENDING 的时候,才允许改成 PAID// 这里的 where status = 'PENDING' 就是乐观锁的精髓introws=orderMapper.updateStatus(orderId,"PAID",// 目标状态"PENDING"// 期望的前置状态);if(rows==1){returntrue;// 支付状态更新成功}else{// 更新失败,说明订单可能已经被定时任务抢先关闭了!// 这时候应该发起退款逻辑,而不是强行改状态returnfalse;}}

同理,定时关单的逻辑也是一样:

-- 只有在订单还是 PENDING 状态时,才允许改成 CLOSEDUPDATEordersSETstatus='CLOSED'WHEREid=10086ANDstatus='PENDING';

谁先抢到谁赢:

  • 如果是支付先到:状态变为PAID。稍后定时任务执行,发现where status = 'PENDING'不满足,更新 0 行,关单失败。(符合预期,用户支付成功)
  • 如果是关单先到:状态变为CLOSED。稍后支付回调执行,发现where status = 'PENDING'不满足,更新 0 行。系统检测到更新失败,发起自动退款。(符合预期,避免了单子关了钱没退的尴尬)

总结

做一个靠谱的交易系统,真的全是细节:

  1. 防超卖:Redis Lua 脚本原子扣减。
  2. 防少卖:警惕 Redis 与 DB 的数据不一致,利用对账或回补机制。
  3. 防状态错乱:状态机定义流转方向,乐观锁(CAS)解决并发冲突。
http://www.cnnetsun.cn/news/86117.html

相关文章:

  • Docker中TensorFlow-GPU部署与CUDA问题解决
  • 大模型学习路线(2026最新)从零基础入门到精通,看完这一篇就够了
  • NPM安装Socket.IO实现实时推送TensorRT状态
  • 手把手教你从0搭建一个智能体,全部跟下来你就Agent入门了!(超详细的讲解)
  • Linly-Talker:支持图片上传的AI数字人对话系统
  • Qwen3-VL-8B实现近实时视频流分析的实践探索
  • Qwen-Image-Edit-2509能否在手机上运行?
  • XTS 认证
  • 22、Perl正则表达式与程序交互全解析
  • GPU加速YOLO推理:TensorRT集成教程
  • 23、深入理解Perl中的函数和子程序
  • 28、Perl高级编程:引用、多维数组与哈希引用
  • 【JavaSE】十七、UDP套接字编程 TCP套接字编程
  • 10个降AI率工具推荐,本科生高效避坑指南
  • 海洋微生物显微图像分类与检测:Yolo13-Seg-Faster模型实现14种物种自动识别
  • 为什么哈希函数能快速定位元素位置?从案例、原理到应用
  • 购票管理系统
  • 防火墙实验 防火墙综合实验
  • AI大模型Agent运维监控面试秘籍:15道高频题+实战解析,助你轻松应对面试挑战(收藏级)!
  • FLUX.1-dev-Controlnet-Union模型对比解析
  • DeepSpar USB Stabilizer: 仅使用软件尝试数据恢复,其背后的风险
  • 为什么计算机生必打 CTF?低门槛 + 高收益全揭秘
  • TensorRT-LLM入门指南:高效推理大模型
  • TOP Server + DataHub 构建高可用工业数据冗余解决方案
  • 镜正理念:从字母“pq”与“bd”看唯悟主义的超越
  • iOS 项目中常被忽略的 Bundle ID 管理问题
  • 企业数据API对接技术选型指南:如何评估与选择技术服务厂商
  • HuggingFace自定义模型接入Anything-LLM指南
  • 惊爆!SubtleCrypto:让Web应用瞬间变身加密堡垒,99%的开发者都忽略了这个神器!
  • 拼接符“II”在Oracle和HGDB中使用的差异