分布式商城系统架构中的超卖问题深度剖析(从问题原因到解决方案到优化总结)以及重复下单问题

超卖问题(包含重复下单问题)

背景

首先,超卖问题的出现是由于高并发环境下,大量秒杀请求同时发给服务端导致的秒杀商品的销售数量>其库存数量的问题。其本质就是并发场景下,多线程或者多进程对共享资源(库存资源)进行竞争导致的(因为MySQL数据库中的数据对于它们来说属于共享资源),因此这种多线程以及多进程环境下对于共享资源的竞争问题我们需要对共享资源进行保护。

问题原因

  1. 判断用户是否登陆,是否有收货地址
  2. 判断库存是否够用
  3. 判断是否已经秒杀到了,防止重复下单
  4. 减库存
  5. 创建订单

流程中可能会出现的问题

  1. 重复下单问题,秒杀应该要求用户(每件秒杀商品)只能下一次单,即秒杀操作应该具有幂等性
  2. 超卖问题,由于步骤2与步骤4并不是原子操作,并发访问下会导致超卖问题

解决方法

重复下单问题(该问题不属于超卖问题,只是拓展用)

对于重复下单问题只需要保证秒杀下单请求这一操作具有幂等性(谁不能重复就针对谁设计幂等性如加一个唯一标识)即可。也就是说需要对秒杀订单请求进行预检。但是这个预检操作基于性能考虑,不能基于MySQL进行订单预检,否则会对数据库造成大量的并发查找压力。因此将订单的预检功能上移到Redis中进行。具体操作就是针对每一个秒杀请求进行预存订单处理,每当有秒杀请求发送到服务器,都会向Redis中插入一条预存订单,其key为一个秒杀前缀比如"sec:product:order:"然后加上用户id和秒杀商品id。然后每次有秒杀请求过来都需要先查询Redis中是否存在预存订单,有就说明是重复(value值=1)下单直接返回下单失败即可。

超卖问题的原因就是查询商品库存与扣减商品库存这两个操作不是原子操作,在并发访问下会出现问题。那么原子操作首先想到的就是使用事务,但是一般来说这种并发环境都是分布式架构。如果通过事务来解决,先更新后查询,然后判断是否超库存,若发生超卖则抛出异常,通过spring事务回滚处理。但它是有明显缺陷的:即spirng事务不能严格保证数据一致性和业务逻辑正确性,而且减库存的压力全部落在数据库身上,不能保证高并发。

为什么说spring事务为什么不能保证数据一致性和业务逻辑正确性??

  1. 如果事务方法抛异常,此时会回滚数据库操作,但已经执行的其他方法不会回滚,因此无法保证业务逻辑正确性;

  2. 即使事务方法不抛异常,也不能保证数据一致性。因为事务里的数据库操作是整个方法执行结束后才提交到数据库,最后提交到数据库的前后很有可能带来数据一致性问题。

不能用事务,那么并发环境我们就只能考虑用锁了,两种锁:乐观锁和悲观锁,但是它们都会将压力下层到数据库身上。最终决定把数据都存到redis,然后尝试了redis分布式锁,发现其并发量并不高,因为redis分布式锁实质是一种分布式悲观锁,它将处理串行化。最终放弃使用,选用Redis的原子操作来进行预减库存来解决超卖,实质是使用了一种分布式非阻塞乐观锁,底层是CAS算法。

下面简要说一下具体的几种解决方法:

悲观锁

悲观锁在单体项目中就是直接对查询库存与修改库存这两个方法加一把锁,或者针对MySQL修改的库存记录加锁。

分布式环境下就需要分布式锁,如Redis实现分布式锁。每次对秒杀商品修改库存时都需要先获得这个秒杀商品的分布式锁之后才可以进行修改,通过锁强行让库存数据的修改操作变为同步的。

乐观锁

本质就是CAS算法,比如可以给数据库数据设置一个版本号,只有修改库存时的版本号等于查询库存时的版本号才允许修改库存。也可以比较修改库存之后的库存数是否大于0.大于0才允许修改。

但是以上的加锁和给数据库进行CAS处理都有缺点。比如分布式锁(其实可以改进为分布式分段锁)在多用户同时下单的时候,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求。数据库CAS会导致数据库的访问压力过大。

因此在项目中我采用了基于Redis的原子操作进行预检库存来解决超卖问题,方案如下:

  1. 首先判断缓存是否存在该秒杀商品
  2. 判断缓存中的秒杀商品库存是否大于0
  3. 使用Redis进行预减库存redisTemplate.opsForValue().increment(key, -1);这个方法对应Redis的INCR命令,它会返回命令执行后的结果。
  4. 对返回结果进行判断是否大于0。大于0则将MySQL库存扣减消息放入消息队列,让MQ进行MySQL与Redis的数据同步,减少响应时间。如果小于0,表示超卖了,需要回滚Redis数据,增加第三步扣减的Redis库存数据。

原子操作就是INCR命令会原子的执行数据运算与结果返回这两步操作。

但是这里我们因为使用到了Redis来操作商品库存,因此需要保证Redis和MySQL的商品库存的数据一致性。方案有提前缓存秒杀商品数据以及在用户访问量不高的时候进行数据同步,也可以通过定时任务检查redis和mysql的数据一致性。。同时因为使用到了Redis预减库存,我们还要保证Redis的高可用,这里用到了Redis哨兵集群。

优化

该方法涉及到的优化思路如下:

通过使用Redis缓存数据库减少了MySQL数据库的访问并且实现预减库存避免线程安全问题;通过Rabbitmq消息队列实现异步下单提高下单请求响应速度优化了用户体验;


感谢耐心看到这里的同学,觉得文章对您有帮助的话希望同学们不要吝啬您手中的赞,动动您智慧的小手,您的认可就是我创作的动力!
之后还会勤更自己的学习笔记,感兴趣的朋友点点关注哦。

你可能感兴趣的:(分布式,系统架构,数据库)