写点什么

秒杀系统设计 - 超卖问题

用户头像
泽睿
关注
发布于: 1 小时前

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

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


表结构设计如下。


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

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

  • 下单时立即扣减库存。

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

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

  • 可以有效避免恶意下单

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

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


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



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

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


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


  1. 查询库存余量SELECT stock FROM "stock_info" WHERE product_id = 200 AND seckill_id = 28;

  2. mysql 这行语句 会锁表但分情况,一般是加 S 锁,如果是有索引,会给行锁,如果没有索引,则给的是表锁。

  3. S 锁不会影响到其他的查询,但是会影响到插入和更新,也就是说,如果你有一个查询很慢,且进行了表锁,你的插入和更新都会被影响到,但不会影响其他的查询,但如果有索引,走的行锁,又不会影响到其他的插入和更新。

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


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


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

解决办法。

  1. 读取和判断过程中加上事务并且使用 for update


事务开始 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. 使用 UPDATE 语句自带的行锁,乐观锁的做法


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

用户头像

泽睿

关注

还未添加个人签名 2017.12.22 加入

还未添加个人简介

评论

发布
暂无评论
秒杀系统设计-超卖问题