2023-06-23 00:24:12
商品秒杀场景下的库存超卖问题指同一商品被多个订单同时扣减库存,导致实际售出数量超过总库存。其核心原因是高并发环境下,多个请求同时通过库存校验并执行扣减操作,数据库或缓存中的库存字段被并发修改,最终出现负值或重复扣减。以下是系统性解决方案:
一、数据库层解决方案1. 行级锁控制通过数据库引擎的行级锁机制,确保同一时间只有一个请求能修改库存记录。SQL示例:
update goods_stock set amount = amount - 1 where id = $id and amount - 1 >= 0将库存字段设置为无符号整数(UNSIGNED),当减库存操作导致数值小于0时,SQL语句会直接报错。适用场景:对数据一致性要求极高且能接受异常处理的场景。
二、缓存层解决方案(Redis)1. List队列模型为每个商品创建独立的Redis List队列,队列长度等于库存数量,每个元素为商品ID。操作流程:
若弹出成功,表示库存充足,允许下单;
若弹出失败(队列为空),表示库存售罄。

利用Redis的DECR或INCR命令实现原子性扣减。操作流程:
若返回值≥0,表示扣减成功;
若返回值为-1,表示库存不足。
流程:用户下单时直接扣减数据库库存,生成待支付订单。
流程:用户下单后仅预留库存,实际扣减在支付成功后执行。
流程:
若预留有效,则完成扣减;
若预留失效,则重新尝试预扣或拒绝支付。
Redis List队列 + 异步下单:

初始化库存:
public void loadGoodsPushRedis(Long goodsId, int stock) { String key = "SeckillGoodsCountList_" + goodsId; Long[] ids = pushIds(stock, goodsId); // 生成商品ID数组 redisTemplate.opsForList().leftPushAll(key, Arrays.asList(ids)); // 压入Redis队列}下单扣减:
public boolean deductStock(Long goodsId) { String key = "SeckillGoodsCountList_" + goodsId; Long result = redisTemplate.opsForList().rightPop(key); // 原子性弹出 return result != null; // 返回是否成功}五、常见问题解答Q:如何避免超卖?A:核心是在扣减库存时加锁(数据库行级锁或Redis原子命令),确保同一时间只有一个请求能修改库存。若订单取消,需及时释放预留库存。
Q:发货后再减库存是否可行?A:不可行。需在下单时锁定库存(如通过预扣或行级锁),发货时再完成最终扣减,否则高并发下必然超卖。
Q:Redis队列模型适合所有场景吗?A:不适合库存量极大的场景(如百万级),因队列会占用大量内存。此时可结合数据库分表或分布式锁优化。
通过以上方案,可有效解决秒杀场景下的库存超卖问题,具体选择需根据业务规模、性能要求和数据一致性需求综合评估。