商城超卖问题的几种解决方案

本文探讨了电商中常见的超卖问题及其原因,主要源于并发操作和数据库默认的库存扣除方式。提出了三种解决方案:1) 使用Redis队列进行库存管理,但可能造成库存无法及时释放;2) 锁定最后库存结合乐观锁,适用于小流量场景,但在高并发时可能导致多数请求失败;3) 锁定库存无乐观锁,确保不出现超卖,更适用于高并发环境。每种方案都有其适用场景和优缺点。

一、描述:

       商城设计的过程中,必然会考虑到一个库存扣除的问题,超卖将会带来一定的损失和麻烦,在某一个时间段的瞬间的流量会造成库存的并发性操作带来库存超额扣除。针对这类的问题,我先来分析原因吧。

二、为什么会被超卖

只有弄清楚为什么会被超卖才能解决超卖问题,这里我们要弄清楚mysql的默认的扣除方式,update 表 store=store-1 where id=指定的id
首先我们看这样的语句有哪些问题呢?

  1. 库存有可能扣到为负数,无法锁定最后的库存
  2. 如果从外面select一次库存,再查找又怕出现主从不一致问题
  3. 并发问题

如何解决以下问题呢?

三、解决方案

1、采用redis队列的方式进行库存增减运算。将创建订单、退款、取消订单做list的相应的增减操作

(1)创建商品、变更数量

  初始化redis的list结构,变更商品数量的时候,redis的list数量做相应的增减。

(2)商品上架、下架

 下架:设置redis的list过期

 上架:初始化redis的list

(3)创建订单成功

  list进行出队列的lpop

(4)取消订单、退款

  list的lpush入队列,进行补偿

(5)库存校验

 判断list的数量-当前的购买的数量 > 0的情况

 否则:提示库存不足

在这里插入图片描述

这种方案,看起来很完美,没啥问题,细心下你会发现,假如一些用户进入队列后,他不想买了,停在页面时间较长,这个就比较不好处理了,直接性导致很多刃余库存无法释放,估计还得写lua来控制了,比较得不偿失。

2、 锁定最后库存+乐观锁

系统中通过事务来判断,保证扣减后数据不为负数,否则回滚

设置数据库字段数据为无符号证书,通过扣减是,字段值小于零触发sql语句报错

select id,store,status,version from 商品表 where id=#1 and status=1,读出库存以及版本

修改库存:update 商品表 set store=store-#1,version=version+1 where status=1 and version=#version and store - #1 》 0

读取库存和版本信息的时候可强行读主库,避免主从不一致的情况,版本信息,

判断:库存-num > 0,版本=当前版本,避免延迟。

缺点:这种方案也是网上最多的方案,看起来很完美的,实际上也是有缺陷的,假如你的接口事务开始到事务结束,所需的时间是200ms,在这个200ms内的所有并发操作,只有一个人是成功的,其它用户因为乐观锁的原因都是失败的。当日了这个是针对并发量较大的情况才会发生的,一般的小流量倒是可以对付的。
在这里插入图片描述

3、 锁定最后库存无乐观锁

系统中通过事务来判断,保证扣减后数据不为负数,否则回滚
反正我不管最终update多少次,只要扣除成功了就行。也就是说我守住最后的底线,这种情况就不会出现一个人成功,其他人都失败的情况。事务过程中实际也是刷新脏页的过程,这种过程主库的update操作实际都是内部有个排队机制的,不会出现超卖的现象。
update 商品表 set store=store-#1 where status=1 and store - #1 》 0

个人认为3方案更加人性化一些,也比较简单,不需要读取过多的信息
1方案成本较高一些,当然了大型项目比较适合
2方案比较严谨一些,避免出现问题的几率较大,大型公司的mysql服务器在这个中间事务开始到操作结束,10ms不到,这种情况下一人成功,其他人失败问题不大。

### 三级标题:商品问题的成因 在商城系统中,尤其是在高并发场景下,如秒杀或限时抢购活动中,商品是一个常见的问题。该问题通常发生在多个请求几乎同时到达服务器,并试图修改同一商品的库存时。由于数据库操作不具备原子性或者并发控制机制不当,导致库存被错误地减少,从而使得实际售出的商品数量过库存上限。 ### 三级标题:悲观锁与乐观锁对比 悲观锁假设最坏的情况总是会发生,因此在处理数据时会锁定数据项[^3]。这种方式适用于写操作较多的场景,但在高并发环境下可能会导致大量的线程阻塞等待,影响系统性能。而乐观锁则假设冲突很少发生,只有在提交更新时才会检查是否有冲突[^1]。这种策略更适合读多写少的场景,可以有效减少锁竞争带来的开销。 ### 三级标题:Redis 原子操作解决 使用 Redis 的原子操作进行预减库存是一种有效的解决方案[^1]。通过利用 Redis 提供的原子命令(例如 `DECR` 或 `INCRBY`),可以在不引入额外锁的情况下保证库存扣减操作的安全性。这种方法避免了传统分布式锁可能导致的串行化瓶颈,提高了系统的并发处理能力。 ### 三级标题:Redis 分布式锁的问题 尽管 Redis 可以用来实现分布式锁,但在某些情况下可能并不理想。比如当一个请求获取到锁后执行业务逻辑的时间过了锁的有效期,那么这个锁会被自动释放,这可能导致其他请求获得锁并同时访问共享资源,进而引发问题[^2]。此外,如果第一个请求完成之后尝试解锁,它可能会错误地释放第二个请求持有的锁,造成进一步的数据不一致风险。 ### 三级标题:优化建议 为了克服上述问题,除了采用 Redis 的原子操作外,还可以考虑以下几点: - **设置合理的时时间**:对于任何类型的锁,都应该根据预期的业务处理时间来设定适当的时值,以防止死锁和不必要的等待。 - **使用唯一标识符**:确保每个加锁请求都有唯一的标识符,在解锁时验证该标识符是否匹配,这样即使锁过期也能保持一致性[^4]。 - **异步处理**:将耗时较长的操作移出关键路径,通过消息队列等方式异步处理,减轻对实时性的要求。 - **分片库存管理**:通过对库存进行水平拆分,为不同的用户群体分配独立的库存池,以此降低单个库存点的压力。 ### 三级标题:代码示例 下面给出一个基于 Redis 原子操作的简单示例,用于演示如何安全地减少库存: ```python import redis def decrease_stock(r: redis.StrictRedis, product_id: str, quantity: int) -> bool: """ 尝试从指定的产品ID中扣除一定数量的库存。 :param r: Redis客户端实例 :param product_id: 产品的唯一标识符 :param quantity: 要扣除的数量 :return: 如果成功扣除了库存返回True;否则返回False """ stock_key = f"stock:{product_id}" current_stock = r.get(stock_key) if not current_stock or int(current_stock) < quantity: return False # 库存不足或不存在 # 使用DECRBY命令尝试减少库存 result = r.decrby(stock_key, quantity) # 检查库存是否足够(虽然这里已经检查过,但这是为了确保原子操作后的状态正确) if result >= 0: return True else: # 如果减完后库存小于零,则恢复原来的库存 r.incrby(stock_key, quantity) return False # 创建Redis连接 r = redis.StrictRedis(host='localhost', port=6379, db=0) # 示例调用函数 product_id = "item_123" quantity_to_decrease = 5 if decrease_stock(r, product_id, quantity_to_decrease): print(f"Successfully decreased {quantity_to_decrease} units of {product_id}.") else: print(f"Failed to decrease {quantity_to_decrease} units of {product_id}. Check stock availability.") ``` 此段代码展示了如何利用 Redis 的 `decrby` 方法来实现库存的安全扣除。请注意,这段代码假定你已经有了一个运行中的 Redis 实例,并且产品库存已经被初始化。
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值