这几天在网上看到一篇讲解—关于PHP解决高并发的博客,就转载过来了
转载地址:https://www.cnblogs.com/walblog/articles/8476579.html
我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。
1. 超发的原因
假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一
2- 解决方案:
方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false
<?php
//优化方案1:将库存字段number字段设为unsigned,当库存为0时,因为字段不能为负数,将会返回false
include('./mysql.php');
$username = 'wang'.rand(0,1000);
//生成唯一订单
function build_order_no(){
return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0,$username){
global $conn;
$sql="insert into ih_log(event,type,usernma)
values('$event','$type','$username')";
return mysqli_query($conn,$sql);
}
function insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number)
{
global $conn;
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price,username,number)
values('$order_sn','$user_id','$goods_id','$sku_id','$price','$username','$number')";
return mysqli_query($conn,$sql);
}
//模拟下单操作
//库存是否大于0
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' ";
$rs=mysqli_query($conn,$sql);
$row = $rs->fetch_assoc();
if($row['number']>0){//高并发下会导致超卖
if($row['number']<$number){
return insertLog('库存不够',3,$username);
}
$order_sn=build_order_no();
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id' and number>0";
$store_rs=mysqli_query($conn,$sql);
if($store_rs){
//生成订单
insertOrder($order_sn,$user_id,$goods_id,$sku_id,$price,$username,$number);
insertLog('库存减少成功',1,$username);
}else{
insertLog('库存减少失败',2,$username);
}
}else{
insertLog('库存不够',3,$username);
}
?>
方案2:使用MySQL的事务,锁住操作的行
<?php
//优化方案2:使用MySQL的事务,锁住操作的行
include('./mysql.php');
//生成唯一订单号
function build_order_no(){
return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values('$event','$type')";
mysqli_query($conn,$sql);
}
//模拟下单操作
//库存是否大于0
mysqli_query($conn,"BEGIN"); //开始事务
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id' FOR UPDATE";//此时这条记录被锁住,其它事务必须等待此次事务提交后才能执行
$rs=mysqli_query($conn,$sql);
$row=$rs->fetch_assoc();
if($row['number']>0){
//生成订单
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
$order_rs=mysqli_query($conn,$sql);
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs=mysqli_query($conn,$sql);
if($store_rs){
echo '库存减少成功';
insertLog('库存减少成功');
mysqli_query($conn,"COMMIT");//事务提交即解锁
}else{
echo '库存减少失败';
insertLog('库存减少失败');
}
}else{
echo '库存不够';
insertLog('库存不够');
mysqli_query($conn,"ROLLBACK");
}
?>
方案4:使用非阻塞的文件排他锁
<?php
//优化方案4:使用非阻塞的文件排他锁
include ('./mysql.php');
//生成唯一订单号
function build_order_no(){
return date('ymd').substr(implode(NULL, array_map('ord', str_split(substr(uniqid(), 7, 13), 1))), 0, 8);
}
//记录日志
function insertLog($event,$type=0){
global $conn;
$sql="insert into ih_log(event,type)
values('$event','$type')";
mysqli_query($conn,$sql);
}
$fp = fopen("lock.txt", "w+");
if(!flock($fp,LOCK_EX | LOCK_NB)){
echo "系统繁忙,请稍后再试";
return;
}
//下单
$sql="select number from ih_store where goods_id='$goods_id' and sku_id='$sku_id'";
$rs = mysqli_query($conn,$sql);
$row = $rs->fetch_assoc();
if($row['number']>0){//库存是否大于0
//模拟下单操作
$order_sn=build_order_no();
$sql="insert into ih_order(order_sn,user_id,goods_id,sku_id,price)
values('$order_sn','$user_id','$goods_id','$sku_id','$price')";
$order_rs = mysqli_query($conn,$sql);
//库存减少
$sql="update ih_store set number=number-{$number} where sku_id='$sku_id'";
$store_rs = mysqli_query($conn,$sql);
if($store_rs){
echo '库存减少成功';
insertLog('库存减少成功');
flock($fp,LOCK_UN);//释放锁
}else{
echo '库存减少失败';
insertLog('库存减少失败');
}
}else{
echo '库存不够';
insertLog('库存不够');
}
fclose($fp);
?>
方案5:Redis中的watch
<?php
$redis = new redis();
$result = $redis->connect('127.0.0.1', 6379);
echo $mywatchkey = $redis->get("mywatchkey");
/*
//插入抢购数据
if($mywatchkey>0)
{
$redis->watch("mywatchkey");
//启动一个新的事务。
$redis->multi();
$redis->set("mywatchkey",$mywatchkey-1);
$result = $redis->exec();
if($result) {
$redis->hSet("watchkeylist","user_".mt_rand(1,99999),time());
$watchkeylist = $redis->hGetAll("watchkeylist");
echo "抢购成功!<br/>";
$re = $mywatchkey - 1;
echo "剩余数量:".$re."<br/>";
echo "用户列表:<pre>";
print_r($watchkeylist);
}else{
echo "手气不好,再抢购!";exit;
}
}else{
// $redis->hSet("watchkeylist","user_".mt_rand(1,99999),"12");
// $watchkeylist = $redis->hGetAll("watchkeylist");
echo "fail!<br/>";
echo ".no result<br/>";
echo "用户列表:<pre>";
// var_dump($watchkeylist);
}*/
$rob_total = 100; //抢购数量
if($mywatchkey<=$rob_total){
$redis->watch("mywatchkey");
$redis->multi(); //在当前连接上启动一个新的事务。
//插入抢购数据
$redis->set("mywatchkey",$mywatchkey+1);
$rob_result = $redis->exec();
if($rob_result){
$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),$mywatchkey);
$mywatchlist = $redis->hGetAll("watchkeylist");
echo "抢购成功!<br/>";
echo "剩余数量:".($rob_total-$mywatchkey-1)."<br/>";
echo "用户列表:<pre>";
var_dump($mywatchlist);
}else{
$redis->hSet("watchkeylist","user_".mt_rand(1, 9999),'meiqiangdao');
echo "手气不好,再抢购!";exit;
}
}
?>