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

一、核心问题分析

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

二、技术方案设计

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模式 + 本地缓存合并 高一致,支持柔性事务
  • 超卖防护:通过原子操作和预占机制确保不超卖
  • 少卖解决:完善的库存释放和补偿机制
  • 高性能:缓存分片+批量合并支撑高并发
  • 高可靠:对账系统兜底数据一致性
相关推荐
阑梦清川2 小时前
关于Go语言的开发环境的搭建
开发语言·后端·golang
lyrhhhhhhhh2 小时前
Spring 模拟转账开发实战
java·后端·spring
tonngw2 小时前
【Mac 从 0 到 1 保姆级配置教程 12】- 安装配置万能的编辑器 VSCode 以及常用插件
git·vscode·后端·macos·开源·编辑器·github
noravinsc3 小时前
InforSuite RDS 与django结合
后端·python·django
Brookty4 小时前
【MySQL】基础知识
后端·学习·mysql
一只码代码的章鱼4 小时前
Spring 的 异常管理的相关注解@ControllerAdvice 和@ExceptionHandler
java·后端·spring
老友@5 小时前
Spring Data Elasticsearch 中 ElasticsearchOperations 构建查询条件的详解
java·后端·spring·elasticsearch·operations
熬夜苦读学习5 小时前
Linux线程控制
linux·运维·服务器·开发语言·后端
bing_1586 小时前
Spring Boot 项目中什么时候会抛出 FeignException?
java·spring boot·后端
Java&Develop6 小时前
springboot + mysql8降低版本到 mysql5.7
java·spring boot·后端