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

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

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

毕竟,库存只有 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]: 要购买的数量

local stock = tonumber(redis.call('get', KEYS[1]))
local amount = tonumber(ARGV[1])

if stock and stock >= amount then
    -- 库存充足
    redis.call('decrby', KEYS[1], amount)
    return 1 -- 成功
else
    return 0 -- 库存不足
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 伪代码感受一下:

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

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

-- 只有在订单还是 PENDING 状态时,才允许改成 CLOSED
UPDATE orders 
SET status = 'CLOSED' 
WHERE id = 10086 AND status = 'PENDING';

谁先抢到谁赢:

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

总结

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

  1. 防超卖: Redis Lua 脚本原子扣减。
  2. 防少卖: 警惕 Redis 与 DB 的数据不一致,利用对账或回补机制。
  3. 防状态错乱: 状态机定义流转方向,乐观锁(CAS)解决并发冲突。
    在这里插入图片描述
在秒杀系统中,少卖是常见的问题。指的是在秒杀活动中售的商品数量过了实际库存数量,而少卖则是指实际库存数量大于秒杀活动中售的商品数量。这两个问题都会给商家和消费者带来一些困扰。 问题可能出现在秒杀活动赶上热销商品时,由于系统处理延迟、并发请求等原因,导致实际销售数量过了库存数量。这会造成一些消费者购买了实际上已经售罄的商品,给消费者带来不满和投诉,同时也对商家的声誉和销售利益造成损害。 少卖问题则是指实际库存数量大于秒杀活动中售的商品数量。这可能是由于系统数据同步延迟、售过程中出现异常等原因导致的。对于商家来说,这会导致潜在的销售机会的浪费,同时也会让消费者失去购买心愿。 为了解决少卖问题,秒杀系统需要做到以下几点: 1. 系统实时库存更新:系统需确保在秒杀活动中及时更新库存数量,避免少卖的情况发生。 2. 并发请求处理:系统需要具备足够的并发处理能力,能够同时处理大量用户的请求,避免因系统处理延迟而导致的问题。 3. 限制购买数量:通过设置购买数量限制,可以避免某些用户一次性购买大量商品,从而平衡库存和需求之间的关系。 4. 实时监控和报警机制:建立实时监控和报警机制,及时发现库存异常变动,以便及时采取措施解决问题。 总之,秒杀系统少卖问题是需要特别留意的,商家需要通过合理的系统设计和管理措施来减少这类问题的发生,以提供良好的用户体验和保护商家的利益。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值