本文采用MySQL下的InnoDB存储引擎实现秒杀,MySQL支持很多存储引擎,每种存储引擎都有不同的特性。若使用的是其他存储存储引擎或者数据库情况将会有所不同。
秒杀有以下几种特点:
1、不能出现超卖。假设秒杀的商品只有100个库存,结果你卖出了101个,甚至更多,在这种情况下,你的秒杀也谈不上秒杀。
2、一个用户只能买一个商品。
3、高性能,直观感受就是快,否则就谈不上秒杀。
首先我们简单的模拟一下场景,建表。
CREATE TABLE `sdb_goods_promotion` (
`goods_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`stock` int(10) unsigned NOT NULL,
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
促销商品表:负责存储需要促销的商品,以及库存量。
CREATE TABLE `sdb_goods_promotion_buyer` (
`uid` int(10) unsigned NOT NULL AUTO_INCREMENT,
`goods_id` int(10) unsigned NOT NULL,
PRIMARY KEY (`uid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
买家订单表:记录买家的购买信息。
大部分人都会这么做:
begin transaction
已卖出数量:select count(goods_id) from sdb_goods_promotion_buyer where goods_id=xxxx
总库存量:select stock from sdb_goods_promotion where goods_id=xxxx
if(已卖出数量>=总库存量){
rollback
}
当前用户已买数量:select count(goods_id) from sdb_goods_promotion_buyer where uid=xxx;
if(当前用户已买数量>=购买限制){
rollback
}
insert into sdb_goods_promotion_buyer(uid,goods_id) value(XXX,XXXX);
commit;
像上面这种做法,把数据读到内存中进行运算,最终入库的操作,既不能避免用户购买次数限制,也不能避免超卖情况。
接下来,我们一一解决购买次数限制以及超卖情况。
1、用户多次购买问题
我们只需要在买家订单表里面添加一个唯一索引(uid,goods_id)就可以解决这个问题。当有用户重复购买时就会跑出异常。从而在数据库层面解决了这个问题。但在代码中我们需要捕获(catch)这个异常,抛出用户便于理解的信息。
2、超卖问题
在sdb_goods_promotion表的goods_id列上加上索引,然后利用悲观锁。select * from sdb_goods_promotion where goods_id=xxx for update;
以上我们就解决了用户多次购买和超卖问题。
但是,如果我们用户体量有几十万或者更大的时候,这种做法是很慢,且不合理的。所以我们还是得继续优化。
在上面的例子中我们为了保证一致性而引入了事务和悲观锁,悲观锁是一种很耗时的操作。但是现在我们为了性能却要牺牲一致性了。
在秒杀中,我们要知道什么能接受 ,什么不能接受。比如在上面的例子中我们不能接受一个用户买多个商品,也不能接受超卖,但是我们是可以接受少卖的。只要我们把少卖的商品误差做到足够小。
在上面的例子中,用户重复购买是不会发生的,因为我们使用MySQL的唯一索引实现。我们为了性能去掉了事务,所以 for upate锁就自然失效了。我们之前使用count()这种方式统计已卖出的数量,然后在内存中进行运算得出剩余的商品数量,进行订单的生成操作。但是现在我们去掉了事务,就再也不能保证一致性了。
所以,我们接下来可以修改一下商品促销表(sdb_goods_promotion):
新增一列already_sales(已卖出的商品数量),表结构如下:
CREATE TABLE `sdb_goods_promotion` (
`goods_id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`already_sales` int(10) unsigned NOT NULL DEFAULT '0',
`stock` int(10) unsigned NOT NULL,
PRIMARY KEY (`goods_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
接下来当有用户进行购买时,我们的库存减少操作如下:
update sdb_goods_promotion set already_sales=already+1 where goods_id=XXXX
如果商品只有1000个,但是同时有2000个人抢购会发生什么情况?
虽然没有了事务,但是update语句会天然的有行锁,前1000个用户都会执行成功,返回生效行数为1,后1000个用户都会失败,虽然不会失败,但是返回生效行数为0。所以在程序中只要判断update的生效行数就知道是否抢购成功了。
但是我们在库存减少和订单生成应该是要保持一致性的,所以当用户重复购买生成订单失败时,我们需要把扣减的商品数量加上去。
$res = update sdb_goods_promotion set already_sales=already+1 where goods_id=XXXX and stock>already_sales
if($res){
try{
insert into sdb_goods_promotion_buyer(uid,goods_id) value(XXX,XXXX);
}catch(\Exection $exection){
update sdb_goods_promotion set already_sales=already-1 where goods_id=XXXX and already_sales-1>0
}
}else{
echo "商品已卖完";exit;
}