库存系统如何避免超卖和少卖?

一、核心问题分析

问题类型 原因 后果
超卖 并发请求下多个线程同时判定库存充足,导致扣减总和超过实际库存 订单无法履约,用户投诉
少卖 已扣减库存未释放(如订单未支付、系统异常回滚失败) 商品滞销,营收损失

二、技术方案设计

1. 数据库层:强一致性控制

方案一:乐观锁(版本号控制)

sql 复制代码
UPDATE sku_stock 
SET stock = stock - #{num}, 
    version = version + 1 
WHERE sku_id = #{skuId} 
  AND version = #{oldVersion} 
  AND stock >= #{num};
  • 优点:无锁竞争,适合中等并发场景
  • 缺点:高并发下大量请求失败需重试

方案二:悲观锁(SELECT ... FOR UPDATE)

sql 复制代码
BEGIN;
SELECT stock FROM sku_stock WHERE sku_id = #{skuId} FOR UPDATE;
-- 业务逻辑校验
UPDATE sku_stock SET stock = stock - #{num} WHERE sku_id = #{skuId};
COMMIT;
  • 优点:强一致性保证
  • 缺点:并发性能差,可能引发死锁

方案三:直接库存约束

sql 复制代码
UPDATE sku_stock 
SET stock = stock - #{num} 
WHERE sku_id = #{skuId} 
  AND stock >= #{num};  -- 核心约束条件
  • 优点:简单高效,依赖数据库原子性
  • 缺点:需处理更新结果为0的失败情况

2. 缓存层:高性能扣减

使用Redis+Lua原子操作

lua 复制代码
-- KEYS[1]:库存key, ARGV[1]:扣减数量
local stock = tonumber(redis.call('GET', KEYS[1]))
if stock >= tonumber(ARGV[1]) then
    redis.call('DECRBY', KEYS[1], ARGV[1])
    return 1  -- 成功
else
    return 0  -- 失败
end
  • 优点:万级TPS,避免数据库压力
  • 补充:需异步同步Redis与数据库数据

3. 业务层:预占库存机制

plaintext 复制代码
         +---------------------+
         |     用户下单          |
         +----------+----------+
                    | 预占库存
         +----------v----------+
         | 生成预占订单(待支付)   |
         +----------+----------+
                    | 支付超时/失败
         +----------v----------+       +------------------+
         | 定时任务释放预占库存  +-------> 恢复Redis+DB库存 |
         +---------------------+       +------------------+
  • 关键点
    • 预占库存有效期(如30分钟)
    • 支付成功后执行实际库存扣减
    • 使用延迟队列(RocketMQ延迟消息)触发释放

4. 分布式环境下的一致性保障

最终一致性方案

plaintext 复制代码
1. 扣减Redis库存成功
2. 发送MQ消息(保证本地事务)
3. 消费者更新数据库库存
4. 定时对账修复差异

强一致性方案(TCC模式)

plaintext 复制代码
Try阶段:
  - 冻结库存(stock_frozen字段+1)
Confirm阶段:
  - 真实扣减库存(stock -= num, stock_frozen -= num)
Cancel阶段:
  - 释放冻结库存(stock_frozen -= num)

5. 少卖问题的专项处理

库存释放策略

  • 未支付订单:通过定时任务扫描超时订单,调用库存回补接口

  • 订单取消:用户主动取消时立即触发库存恢复

  • 补偿机制

    java 复制代码
    // 幂等库存回补接口
    @Transactional
    public void restoreStock(Long skuId, Integer num) {
        skuStockDao.updateStock(skuId, num); // 累加库存
        orderDao.markStockRestored(orderId); // 标记避免重复回补
    }

三、高并发优化策略

1. 热点库存分片

java 复制代码
// 对skuId取模分片到不同Redis节点
int shard = skuId % 16;
String redisKey = "stock:shard_" + shard + ":" + skuId;

2. 本地缓存+批量合并

plaintext 复制代码
服务内存维护一个ConcurrentHashMap:
- Key: skuId
- Value: 待扣减数量(累计合并请求)
定时每100ms批量提交到Redis

3. 令牌桶限流

java 复制代码
// 每个SKU分配独立令牌桶
RateLimiter rateLimiter = RateLimiter.create(1000); // 每秒1000次
if (rateLimiter.tryAcquire()) {
    // 允许执行库存操作
}

四、容灾与监控

1. 库存对账系统

plaintext 复制代码
每日凌晨对比:
Redis库存总量 + 预占库存 = 数据库库存总量
若不相等,触发告警并自动修复

2. 监控指标

  • 实时看板
    • 库存变更QPS
    • 预占库存释放率
    • 库存对账差异数
  • 报警规则
    • 库存扣减失败率 > 1%
    • 库存回补延迟 > 5分钟

3. 熔断降级

plaintext 复制代码
当数据库响应时间 > 500ms时:
1. 切换库存计算到Redis-only模式
2. 记录日志后异步补偿

五、方案选型建议

场景 推荐方案 优点
普通电商(千级TPS) 数据库乐观锁 + 预占机制 实现简单,数据强一致
秒杀系统(万级TPS) Redis分片 + 异步对账 高性能,可扩展
分布式复杂业务 TCC模式 + 本地缓存合并 高一致,支持柔性事务
  • 超卖防护:通过原子操作和预占机制确保不超卖
  • 少卖解决:完善的库存释放和补偿机制
  • 高性能:缓存分片+批量合并支撑高并发
  • 高可靠:对账系统兜底数据一致性
相关推荐
L2ncE1 小时前
双非计算机自救指南(找工作版)
后端·面试·程序员
cdg==吃蛋糕1 小时前
solr自动建议接口简单使用
后端·python·flask
Joseit2 小时前
基于 Spring Boot实现的图书管理系统
java·spring boot·后端
{⌐■_■}2 小时前
【go】什么是Go语言的GPM模型?工作流程?为什么Go语言中的GMP模型需要有P?
java·开发语言·后端·golang
IT杨秀才2 小时前
LangChain框架入门系列(5):Memory
人工智能·后端·langchain
程序猿chen3 小时前
JVM考古现场(二十四):逆熵者·时间晶体的永恒之战
java·jvm·git·后端·程序人生·java-ee·改行学it
AronTing3 小时前
单例模式:确保唯一实例的设计模式
java·javascript·后端
AronTing3 小时前
模板方法模式:定义算法骨架的设计模式
java·后端·面试
AronTing3 小时前
迭代器模式:统一数据遍历方式的设计模式
java·后端·面试
AronTing3 小时前
策略模式:动态切换算法的设计智慧
java·后端·面试