利用分布式锁解决商品秒杀(抢购)中的商品超卖问题
上篇文章简单说了一下“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的并发量。执行完之后我们来看下订单表中的记录:
数据库中订单记录竟然产生了18条!多产生了8个订单。而我们再看一下现在的库存:
该鼠标的库存从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先生个人博客 所有,欢迎分享本文,转载请保留出处,谢谢!
文章评论
评论列表
http://www.vip7388.com/
感谢,感谢,用到了
当启蒙不错