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

表结构设计如下。

图中的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崩溃。这一块内让我们下一期聊。