K先生个人博客

当前位置:首页 > 爱技术 > 正文

利用分布式锁解决商品秒杀(抢购)中的商品超卖问题

上篇文章简单说了一下“PHP中redis的商品秒杀、抢购是怎么实现的”,那么这篇文章依然是分析商品秒杀(抢购)中的商品超卖问题,但方法会和之前不太一样。并且这次会重点分析超卖问题。

模拟商品超卖问题的产生

数据表还和上一篇文章的一样,一个商品表,一个订单记录表。商品秒杀(抢购)的业务代码如下:

<?php
$uid = uniqid(); //生成一个用户id
$conn = mysqli_connect('localhost', 'root', '123456', 'study');
if (!$conn) {
	die("连接失败: " . mysqli_connect_error());
}
$result = mysqli_fetch_assoc(mysqli_query($conn, "SELECT stock FROM goods where id=1"));//查出参与秒杀的商品库存
if ($result["stock"] > 0) {
	sleep(1); //休眠一秒,模拟真实下单延迟
	mysqli_query($conn, "UPDATE goods SET stock =stock-1 where id=1 "); //库存减1
	mysqli_query($conn, "INSERT into `order` (`uid`,`rank`) values('" . $uid . "'," . $result["stock"] . ")");//生成订单记录
} else {
	return '卖完了';
}
mysqli_close($conn);
?>

上面的代码应该都可以看懂,这就算是一个最最简单的商品下单过程了,下面我们利用ab压测工具模拟一下高并发秒杀(抢购)环境,看一下数据库中会有多少订单。

这是商品表中的记录,假设该鼠标参与秒杀的商品数量是10,

模拟超卖之前商品表中的记录

执行ab命令:ab -n 1000 -c 1000 http://localhost/test.php

还是1000次请求,1000的并发量。执行完之后我们来看下订单表中的记录:

image.png

数据库中订单记录竟然产生了18条!多产生了8个订单。而我们再看一下现在的库存:

image.png

该鼠标的库存从10变成了-8.。。。。假如商家拿苹果手机做营销1元抢购,本来打算就送10台的,结果产生了18个订单。。。估计老板会立马提刀取找程序员哈哈哈。

到这,商品秒杀(抢购)中的商品超卖问题应该都明白是怎么回事了吧。

那么该怎么解决呢?上篇文章说的里面redis列表lpop的原子性可以解决(推荐该方法),那么这说另外一种方法,利用分布式锁来解决超卖问题(不推荐)。

利用分布式锁解决商品秒杀(抢购)中的商品超卖问题

这里首先修改一下代码:

<?php
$redis = new Redis();
$redis->connect('127.0.0.1', 6379);
$uid = uniqid(); //生成一个用户id
$conn = mysqli_connect('localhost', 'root', '123456', 'study');
if (!$conn) {
	die("连接失败: " . mysqli_connect_error());
}

$sql = "SELECT stock FROM goods where id=1";
$result = mysqli_fetch_assoc(mysqli_query($conn, $sql));

if ($result["stock"] > 0) {
	$islock = $redis->setnx('mouse', 'K先生');//加锁
	if (!$islock) {
		return '系统繁忙,请稍后再试!';
	}
	mysqli_query($conn, "UPDATE goods SET stock =stock-1 where id=1 ");
	mysqli_query($conn, "INSERT into `order` (`uid`,`rank`) values('" . $uid . "'," . $result["stock"] . ")");
	$redis->delete('mouse');//解锁
} else {
	return '卖完了';
}
mysqli_close($conn);

?>

这里说的分布式锁也是通过redis来实现的,这里主要是利用redis的setnx来实现的。setnx是只要该键值不存在的时候才能设置成功。我们利用其实现简单分布式锁的原理就是,我们使用其设置一个值,当其一直存在的时候,我们再给它赋值是不成功的,当我们执行完业务代码时,再把该值给删除,也就相当于解锁。

当然上面的分布式锁是不完善的,比如说当我们加锁之后,执行业务代码的时候程序挂了,还没解锁。那么程序就会出现死锁的问题,这可以通过捕获业务代码的异常来执行解锁操作来解决(这里就不实现了),再比如说做的分布式机器中的其中一台挂了,也会出现问题,那么这可以在加锁的时候同时设置过期时间来解决,让redis定时清空值解锁就行了。

$islock = $redis->set('mouse', 'K先生', ['nx', 'ex' => 10]); //同时设置过期时间

上面代码就算setnx加上过期时间的写法。

最后要说的是,这种方法不太推荐是因为,这种分布式锁会影响性能,也就是说,如果第一个用户在下单处于加锁状态,那么第二个用户访问就会提示“系统繁忙,请稍后再试!”,只有等第一个用户解锁之后,第二个用户才能正常参与抢购。

那么既然不推荐为啥还要说这种方法呢,因为在特殊场景下,该方法还是比较有用的!

作者K先生本文地址http://www.gold404.cn/info/128

版权声明:本文为原创文章,版权归 K先生个人博客 所有,欢迎分享本文,转载请保留出处,谢谢!

文章评论

* 必填
可选
可选

评论列表