2026-01-15 18:44:13
电商并发减库存设计中,可通过Redis队列结合数据库唯一索引优化实现不超卖,同时兼顾高并发性能。 以下是具体设计思路与方案对比:
一、核心设计思路Redis队列预加载库存
开团时将商品库存数量同步至Redis队列(如goods:{id}:stock),队列长度代表剩余库存。
用户请求时优先检查队列长度:
若队列长度>0,移除一个元素(原子操作),同时异步更新数据库库存。
若队列长度≤0,直接返回“无库存”,避免数据库查询。
优势:Redis原子操作保证库存扣减的准确性,减少数据库压力。
数据库层防重复购买与超卖
判重表(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条,减少数据库交互次数。
高性能:无阻塞操作,可支撑高并发。
Redis队列初始化
开团时通过定时任务或手动触发,将商品库存数量加载至Redis队列:
LPUSH goods:{id}:stock "1" # 假设库存为1用户请求处理
步骤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,回滚事务并返回对应错误。
异步同步库存
为避免Redis与数据库库存不一致,可定期将Redis队列长度同步至数据库(如通过消息队列或定时任务)。
通过上述设计,可实现高并发场景下的库存精准扣减,同时保证系统性能与数据一致性。