电商并发减库存设计,如何做到不超卖

电商并发减库存设计,如何做到不超卖
最新回答
摘星入眼

2026-01-15 18:44:13

电商并发减库存设计中,可通过Redis队列结合数据库唯一索引优化实现不超卖,同时兼顾高并发性能。 以下是具体设计思路与方案对比:

一、核心设计思路
  1. Redis队列预加载库存

    开团时将商品库存数量同步至Redis队列(如goods:{id}:stock),队列长度代表剩余库存。

    用户请求时优先检查队列长度:

    若队列长度>0,移除一个元素(原子操作),同时异步更新数据库库存。

    若队列长度≤0,直接返回“无库存”,避免数据库查询。

    优势:Redis原子操作保证库存扣减的准确性,减少数据库压力。

  2. 数据库层防重复购买与超卖

    判重表(buy_record):记录用户购买行为,字段包括id、uid、goods_id、addtime。

    商品表(goods):存储商品信息,字段包括goods_id、goods_num。

    关键优化:对(uid, goods_id)字段组合添加唯一索引,防止重复购买。

二、方案对比与优化方案一:纯数据库事务(易超卖)
  • 流程:

    查询判重表,判断用户是否已购买。

    插入购买记录。

    查询商品库存。

    更新库存(goods_num-1)。

  • 问题:

    高并发下,多个线程可能同时通过库存检查(如库存为1时),导致超卖。

    判重查询同样存在并发问题,无法保证唯一性。

  • 结论:不可用,高并发场景下必然超卖。
方案二:数据库排他锁(性能低)
  • 流程:

    对判重表查询加FOR UPDATE排他锁。

    插入购买记录。

    对商品表查询加FOR UPDATE排他锁。

    更新库存。

  • 问题:

    排他锁导致所有查询阻塞,并发性能急剧下降。

    数据库成为瓶颈,无法支撑高并发场景。

  • 结论:不推荐,牺牲性能换取准确性。
方案三:唯一索引+条件更新(最优解)
  • 流程:

    插入判重表:直接执行INSERT,若因唯一索引冲突(用户已购买)则抛异常。

    更新商品库存:执行UPDATE goods SET goods_num=goods_num-1 WHERE goods_id=$goods_id AND goods_num>0,若受影响行数为0则抛异常(库存不足)。

  • 优势:

    唯一索引:天然保证用户购买行为的唯一性,无需额外锁。

    条件更新:goods_num>0条件确保库存不会减至负数。

    SQL精简:从4条减少至2条,减少数据库交互次数。

    高性能:无阻塞操作,可支撑高并发。

  • 结论:推荐方案,平衡准确性与性能。
三、完整技术实现
  1. Redis队列初始化

    开团时通过定时任务或手动触发,将商品库存数量加载至Redis队列:

    LPUSH goods:{id}:stock "1" # 假设库存为1
  2. 用户请求处理

    步骤1:检查Redis队列长度:

    LLEN goods:{id}:stock

    若返回0,直接返回“无库存”。

    若返回>0,执行步骤2。

    步骤2:原子移除队列元素:

    RPOP goods:{id}:stock

    步骤3:数据库操作(事务内执行):

    START TRANSACTION;-- 插入判重记录(唯一索引保证唯一性)INSERT INTO buy_record (uid, goods_id, addtime) VALUES ($uid, $goods_id, NOW());-- 更新库存(条件更新防止超卖)UPDATE goods SET goods_num = goods_num - 1 WHERE goods_id = $goods_id AND goods_num > 0;COMMIT;

    若唯一索引冲突或更新受影响行数为0,回滚事务并返回对应错误。

  3. 异步同步库存

    为避免Redis与数据库库存不一致,可定期将Redis队列长度同步至数据库(如通过消息队列或定时任务)。

四、关键注意事项
  1. Redis原子性:使用LPUSH/RPOP等原子操作保证队列扣减的准确性。
  2. 数据库事务隔离级别:建议使用READ COMMITTED,避免脏读且减少锁冲突。
  3. 唯一索引冲突处理:捕获唯一索引异常(如MySQL的Duplicate entry),转换为业务逻辑中的“已购买”提示。
  4. 限流与降级:在Redis队列耗尽时,直接返回“无库存”,避免数据库过载。

通过上述设计,可实现高并发场景下的库存精准扣减,同时保证系统性能与数据一致性。