库存扣减设计有哪些方案

wen IT资讯 1

本文目录导读:

库存扣减设计有哪些方案

  1. 方案一:数据库直接扣减(乐观锁/悲观锁)
  2. 方案二:Redis 缓存扣减 + 异步同步数据库
  3. 方案三:预扣库存(Redis + 数据库事务补偿)
  4. 方案四:最终一致性方案(消息队列)
  5. 方案五:流量漏斗式分层设计(大型项目标配)
  6. 总结对比表
  7. 避坑建议

库存扣减是电商、票务等系统中最核心也最棘手的问题之一,设计不当会导致超卖(用户体验差)或少卖(库存浪费)。

以下整理了几种主流方案,按性能从低到高复杂度从低到高排序,并分析了各自的优缺点和适用场景。


数据库直接扣减(乐观锁/悲观锁)

这是最基础、最直接的方案,直接在关系型数据库层面保证原子性。

乐观锁(最常见)

利用数据库的UPDATE ... WHERE stock > 0来保证不超卖。

  • SQL示例: UPDATE inventory SET stock = stock - 1 WHERE sku_id = ? AND stock > 0;
  • 原理: 数据库的行锁机制保证了这条SQL的原子性,如果stock数量不足,更新影响行数为0,代码返回扣减失败。
  • 优点: 实现简单,不需要额外组件,数据绝对准确。
  • 缺点: 性能瓶颈在数据库,高并发下,大量请求会争抢行锁,导致TPS下降,数据库连接池打满后,系统可能雪崩。

悲观锁(SELECT ... FOR UPDATE)

  • 原理: 查询库存时直接加锁,其他查询必须等待。
  • 优缺点: 比乐观锁更简单,但性能极差,基本不用于高并发场景,容易造成死锁。

适用于低并发(如后台管理系统、企业ERP)对一致性要求极高且流量不大的场景。


Redis 缓存扣减 + 异步同步数据库

这是目前互联网公司处理高并发秒杀、抢购的主流方案,核心思路是:把扣库存的操作从数据库搬到内存(Redis)中执行

  • 核心命令: DECR stock(自减1) 或 EVAL 执行 Lua 脚本。
  • 流程:
    1. 启动时,将数据库的库存加载到 Redis(SET sku:1001:stock 10)。
    2. 用户请求到来,通过 Redis 的原子命令扣减库存。
    3. 扣减成功(返回大于等于0),则继续处理后续逻辑(如创建订单);扣减失败,则返回“已售罄”。
    4. 异步线程/队列,将 Redis 中的成功扣减记录逐步同步回数据库。

为什么用 Lua 脚本?

避免“读库存 -> 判断 -> 扣减”这个三步操作因为高并发出现并发问题,利用 Redis 单线程特性,将三步封装在一个 Lua 脚本里执行,保证原子性。

  • 优点: 性能极高(单机QPS可达10万+),能抗住瞬时峰值流量。
  • 缺点:
    • 数据弱一致: Redis 是内存数据库,可能宕机丢数据。
    • 复杂问题: 需要处理缓存与数据库的一致性,可能需要回滚(Redis 扣了,但后续用户取消订单,需要加回 Redis 库存)。
    • 操作复杂性: 需要额外维护 Redis 集群。

适用于高并发、但允许极小概率数据不一致(可在后续补偿)的场景。


预扣库存(Redis + 数据库事务补偿)

这算是对方案二的增强,用于处理“用户下单后不支付占着库存”的问题。

  • 逻辑:

    1. 用户下单时,在 Redis 中执行预扣(如 DECR)。
    2. 预扣成功,生成一个预占库存的订单(状态为“待支付”),将信息写入 MQ 或异步落库。
    3. 如果用户支付成功: 数据库正式扣减,订单状态改为“已支付”。
    4. 如果用户未支付/超时取消: 系统需要回补库存,即把 Redis 的库存加回来,并修改数据库中的订单状态为“已取消”。
  • 优点: 解决了“恶意下单刷库存”的问题,更接近真实业务逻辑。

  • 缺点: 引入了“回补”逻辑,如果回补失败,库存就会“丢失”,需要额外的定时任务或延迟队列来处理超时订单。

适用于电商平台、演唱会购票等需要有支付环节且需防止黄牛占用库存的场景。


最终一致性方案(消息队列)

该方案的核心思想是 “请求入队,异步串行化处理”

  • 流程:

    1. 用户发起扣库存请求,系统不直接操作数据库,而是将请求(包含用户ID、SKU ID、数量)写入一个消息队列(如 Kafka、RocketMQ)。
    2. 一个独立的库存消费者单线程(或分区有序)消费这个队列中的消息。
    3. 消费者收到消息后,执行数据库的库存扣减。
    4. 扣减成功,通知用户;扣减失败,则需要进行重试或回滚上游。
  • 优点:

    • 削峰填谷: 能应对极高的请求峰值,系统压力平稳。
    • 数据最终一致: 只要消息不被丢失,最终数据一定是准确的。
  • 缺点:

    • 延迟: 用户无法立刻知道是否扣减成功(通常需要等待几秒到几十秒)。
    • 系统复杂: 需要引入消息队列中间件,处理消息重复、丢失、补偿等问题。

适用于对实时性要求不高,但必须保证数据最终不超卖的场景(如部分后台订单处理、库存调拨)。


流量漏斗式分层设计(大型项目标配)

现实中高并发系统通常不会只用一种方案,而是组合上述策略,形成“漏斗”结构。

用户请求
    |
    v
【第一层:本地内存限流/布隆过滤器】
    | (挡住无效请求,如请求一个不存在的商品ID)
    v
【第二层:Redis 原子扣减(高速屏障)】
    | (挡住大部分超出库存的请求)
    v
【第三层:消息队列/异步写库】
    | (将成功的请求串行化,落库持久化)
    v
【第四层:数据库最终一致性写入】
    | (用乐观锁 `UPDATE stock > 0` 做兜底保证)

核心原则:

  1. 动静分离: 商品详情页、库存静态化。
  2. 流量管控: 在前端/网关层就拦截无效请求(同一个用户1秒内多次点击,只放行第一次)。
  3. 缓存先行: 99%的流量在 Redis 层就被处理掉或拒绝了。
  4. 数据库兜底: 数据库的乐观锁作为最后一道防线,保证绝对不会超卖。

总结对比表

方案 核心组件 性能 一致性 复杂度 典型场景
数据库乐观锁 MySQL 强一致性 后台管理、低并发业务
Redis原子扣减 Redis + DB 最终一致性 秒杀、抢购、抽奖
预扣+补偿 Redis + MQ + DB 最终一致性 电商下单、票务(需支付)
消息队列 Kafka/RocketMQ + DB 最终一致性 异步库存同步、跨系统
分层漏斗 L1:限流 L2:Redis L3:MQ L4:DB 极高 最终一致性 极高 双11、高并发大型系统

避坑建议

  1. 不要只用数据库做高并发扣减。 数据库的锁和磁盘IO扛不住。
  2. Redis 方案必须考虑数据丢失。 要做好库存的定期持久化(RDB/AOF),并建立库存校正机制(比如每日凌晨比对 Redis 和 DB 的库存差)。
  3. 任何方案都要有“反作弊”设计。 比如限制单用户购买数量、风控拦截等。
  4. 扣减和支付要联动。 不要下单就扣死库存,要设计“超时释放”或“支付才真正扣除”的策略。

综合来看,对于大多数业务,方案二(Redis扣减 + 异步同步数据库) 是性价比最高的选择,如果业务非常复杂且需要支付环节,可以考虑方案三

抱歉,评论功能暂时关闭!