Bootstrap

秒杀系统设计-超卖问题

秒杀系统设计中最主要的部分就是如何避免超卖问题,因为一旦超卖那么商家就会有所损失,所以秒杀系统如何避免超卖便是重中之重。

通常一个典型的秒杀流程如下所示。

表结构设计如下。

图中的stock_info 表中的锁定标识,之所以有这个标识是用来锁定库存,当用户下单之后进入支付倒计时阶段,如果倒计时结束,这个商品就变为未锁定状态,如果下单完成,这个锁定也要减去 stock - 1, 这里可以思考一下为什么不直接扣减库存?

其实这里涉及的是扣减库存的时机问题?有三种扣减时机?

  • 下单时立即扣减库存。

  • 如上图中下单时候,不等待付款成功,这种用户体验最好,控制最精准,只要下单成功,利用数据库锁机制,用户一定能成功付款,可能被恶意下单。下单后不付款,别人也无法购买了。

  • 先下单,不减库存。实际支付成功后减库存。

  • 可以有效避免恶意下单

  • 对用户体验差,因为下单时没有减库存,可能造成用户下单成功但无法付款。

  • 下单后锁定库存,支付成功后,在减库存。

对于以上三种方案。显然最后一种方案是折中选择。我们先锁定库存,等待用户支付。支付完成之后在进行实际的扣减库存,如果超时未支付会将锁定状态释放。这也是12306购票时采用的方式、先给我们生成订单锁定座位号,等待30分钟支付时间,如果超时未支付就会释放座位。这样就避免了其他人无法购买的问题。如下图所示。

在回到本文重点,如何避免超卖问题。

追踪用户秒杀的整个数据流可以发现、用户操作设计的db操作如下图所示。

抽象出来用户操作的数据模型就是用户通过select语句查询seckill_info、product_info、stock_info 表获取库存信息,然后扣减库存。超卖的问题就是在这里产生的。

并发导致超卖问题如何产生的?

假设现在只剩下一个stock,然后有两个请求同时进来,同时运行select语句,同时得到1的结果. 然后请求1先运行了update得到stock=0,这时候请求2也运行了update就使得stock=1,此时就产生了超卖问题

解决办法。

事务开始 
START TRANSACTION;

查询库存余量,并锁住数据
SELECT stock FROM "stock_info" WHERE product_id = 200 AND seckill_id = 28 FOR UPDATE;

扣减库存
UPDATE "stock_info" set stock = stock - 1 WHERE product_id = 200 AND seckill_id = 28;

事务提交
commit;

事务的第一重要性 原子性要么全无要么全有,使用start transaction和 commit transaction只有原子性的保证,其他不保证。

这里的select 中之所以添加for update行锁的原因是for Update是一个写锁,这里锁住stock_info表,使得其他并发对此表的"修改"操作都block住,带有锁的"select" 也会block。但是正常的不带锁的select 操作可以正常读取到数据。

   1. 查询库存量
   SELECT stock FROM "stock_info" WHERE product_id = 200 AND seckill_id = 28;
   2. 扣减库存
   UPDATE "stock_info" SET stock = stock - 1 WHERE product_id = 200 AND seckill_id = 28 AND stock > 0 ;
   //这里其实就是乐观锁的做法,这里UPDATE语句中的stock不是上面查询语句中stock值,而是此时数据里存的stock值,如果是上面的select值可能造成数据不一致。

超卖问题解决了,那秒杀系统的其他问题呢?

对于大量的请求都访问MySQL了,导致MySQL崩溃。这一块内让我们下一期聊。