秒杀系统设计 - 超卖问题
秒杀系统设计中最主要的部分就是如何避免超卖问题,因为一旦超卖那么商家就会有所损失,所以秒杀系统如何避免超卖便是重中之重。
通常一个典型的秒杀流程如下所示。
表结构设计如下。
图中的 stock_info 表中的锁定标识,之所以有这个标识是用来锁定库存,当用户下单之后进入支付倒计时阶段,如果倒计时结束,这个商品就变为未锁定状态,如果下单完成,这个锁定也要减去 stock - 1, 这里可以思考一下为什么不直接扣减库存?
其实这里涉及的是扣减库存的时机问题?有三种扣减时机?
下单时立即扣减库存。
如上图中下单时候,不等待付款成功,这种用户体验最好,控制最精准,只要下单成功,利用数据库锁机制,用户一定能成功付款,可能被恶意下单。下单后不付款,别人也无法购买了。
先下单,不减库存。实际支付成功后减库存。
可以有效避免恶意下单
对用户体验差,因为下单时没有减库存,可能造成用户下单成功但无法付款。
下单后锁定库存,支付成功后,在减库存。
对于以上三种方案。显然最后一种方案是折中选择。我们先锁定库存,等待用户支付。支付完成之后在进行实际的扣减库存,如果超时未支付会将锁定状态释放。这也是 12306 购票时采用的方式、先给我们生成订单锁定座位号,等待 30 分钟支付时间,如果超时未支付就会释放座位。这样就避免了其他人无法购买的问题。如下图所示。
在回到本文重点,如何避免超卖问题。
追踪用户秒杀的整个数据流可以发现、用户操作设计的 db 操作如下图所示。
抽象出来用户操作的数据模型就是用户通过 select 语句查询 seckill_info、product_info、stock_info 表获取库存信息,然后扣减库存。超卖的问题就是在这里产生的。
查询库存余量
SELECT stock FROM "stock_info" WHERE product_id = 200 AND seckill_id = 28;
mysql 这行语句 会锁表但分情况,一般是加 S 锁,如果是有索引,会给行锁,如果没有索引,则给的是表锁。
S 锁不会影响到其他的查询,但是会影响到插入和更新,也就是说,如果你有一个查询很慢,且进行了表锁,你的插入和更新都会被影响到,但不会影响其他的查询,但如果有索引,走的行锁,又不会影响到其他的插入和更新。
扣减库存
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,此时就产生了超卖问题
解决办法。
读取和判断过程中加上事务并且使用 for update
事务的第一重要性 原子性要么全无要么全有,使用 start transaction 和 commit transaction 只有原子性的保证,其他不保证。
这里的 select 中之所以添加 for update 行锁的原因是 for Update 是一个写锁,这里锁住 stock_info 表,使得其他并发对此表的"修改"操作都 block 住,带有锁的"select" 也会 block。但是正常的不带锁的 select 操作可以正常读取到数据。
使用 UPDATE 语句自带的行锁,乐观锁的做法
超卖问题解决了,那秒杀系统的其他问题呢?
对于大量的请求都访问 MySQL 了,导致 MySQL 崩溃。这一块内让我们下一期聊。
评论