PHP解决高并发问题

PHP 专栏收录该内容
8 篇文章 0 订阅

举个例子,高速路口,1秒钟来5部车,每秒通过5部车,高速路口运作正常。突然,这个路口1秒钟只能通过4部车,车流量仍然依旧,结果必定出现大塞车。(5条车道忽然变成4条车道的感觉)

同理,某一个秒内,20*500个可用连接进程都在满负荷工作中,却仍然有1万个新来请求,没有连接进程可用,系统陷入到异常状态也是预期之内。

14834077821.jpg

其实在正常的非高并发的业务场景中,也有类似的情况出现,某个业务请求接口出现问题,响应时间极慢,将整个Web请求响应时间拉得很长,逐渐将Web服务器的可用连接数占满,其他正常的业务请求,无连接进程可用。

更可怕的问题是,是用户的行为特点,系统越是不可用,用户的点击越频繁,恶性循环最终导致“雪崩”(其中一台Web机器挂了,导致流量分散到其他正常工作的机器上,再导致正常的机器也挂,然后恶性循环),将整个Web系统拖垮。

重启与过载保护

如果系统发生“雪崩”,贸然重启服务,是无法解决问题的。最常见的现象是,启动起来后,立刻挂掉。这个时候,最好在入口层将流量拒绝,然后再将重启。如果是redis/memcache这种服务也挂了,重启的时候需要注意“预热”,并且很可能需要比较长的时间。

秒杀和抢购的场景,流量往往是超乎我们系统的准备和想象的。这个时候,过载保护是必要的。如果检测到系统满负载状态,拒绝请求也是一种保护措施。在前端设置过滤是最简单的方式,但是,这种做法是被用户“千夫所指”的行为。更合适一点的是,将过载保护设置在CGI入口层,快速将客户的直接请求返回

高并发下的数据安全

我们知道在多线程写入同一个文件的时候,会存现“线程安全”的问题(多个线程同时运行同一段代码,如果每次运行结果和单线程运行的结果是一样的,结果和预期相同,就是线程安全的)。如果是MySQL数据库,可以使用它自带的锁机制很好的解决问题,但是,在大规模并发的场景中,是不推荐使用MySQL的。秒杀和抢购的场景中,还有另外一个问题,就是“超发”,如果在这方面控制不慎,会产生发送过多的情况。我们也曾经听说过,某些电商搞抢购活动,买家成功拍下后,商家却不承认订单有效,拒绝发货。这里的问题,也许并不一定是商家奸诈,而是系统技术层面存在超发风险导致的。

  1. 超发的原因

假设某个抢购场景中,我们一共只有100个商品,在最后一刻,我们已经消耗了99个商品,仅剩最后一个。这个时候,系统发来多个并发请求,这批请求读取到的商品余量都是99个,然后都通过了这一个余量判断,最终导致超发。(同文章前面说的场景)

14834077822.jpg

在上面的这个图中,就导致了并发用户B也“抢购成功”,多让一个人获得了商品。这种场景,在高并发的情况下非常容易出现。

优化方案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);
   }
 ?>
  1. 悲观锁思路

解决线程安全的思路很多,可以从“悲观锁”的方向开始讨论。

悲观锁,也就是在修改数据的时候,采用锁定状态,排斥外部请求的修改。遇到加锁的状态,就必须等待。

14834077833.jpg

虽然上述的方案的确解决了线程安全的问题,但是,别忘记,我们的场景是“高并发”。也就是说,会很多这样的修改请求,每个请求都需要等待“锁”,某些线程可能永远都没有机会抢到这个“锁”,这种请求就会死在那里。同时,这种请求会很多,瞬间增大系统的平均响应时间,结果是可用连接数被耗尽,系统陷入异常。

优化方案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");
 }
 ?>
  1. FIFO队列思路

那好,那么我们稍微修改一下上面的场景,我们直接将请求放入队列中的,采用FIFO(First Input First Output,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

14834077834.jpg

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终Web系统平均响应时候还是会大幅下降,系统还是陷入异常。

  1. 文件锁的思路
    对于日IP不高或者说并发数不是很大的应用,一般不用考虑这些!用一般的文件操作方法完全没有问题。但如果并发高,在我们对文件进行读写操作时,很有可能多个进程对进一文件进行操作,如果这时不对文件的访问进行相应的独占,就容易造成数据丢失

优化方案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);
  ?>
 <?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);
  ?>
  1. 乐观锁思路

这个时候,我们就可以讨论一下“乐观锁”的思路了。乐观锁,是相对于“悲观锁”采用更为宽松的加锁机制,大都是采用带版本号(Version)更新。实现就是,这个数据所有请求都有资格去修改,但会获得一个该数据的版本号,只有版本号符合的才能更新成功,其他的返回抢购失败。这样的话,我们就不需要考虑队列的问题,不过,它会增大CPU的计算开销。但是,综合来说,这是一个比较好的解决方案。

在这里插入图片描述
有很多软件和服务都“乐观锁”功能的支持,例如Redis中的watch就是其中之一。通过这个实现,我们保证了数据的安全。

优化方案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;
     }
 }
 ?>
  • 12
    点赞
  • 0
    评论
  • 45
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

包含安装包及源码包 ----------------------------------技术背景介绍===================================' 本系统在一些大中型企业(跨多达24个区域)一直都在很好的服务过,主要在FTP 控制 UDP控制 HTTP控制 傁客户数据库 数据库 数据压缩加密方面进行了综合深入的应用 =====================================模块功能介绍==================================== OAEnterPrise.exe: 用户及权限的集成管理 ;报表种类及字段自动定义 sky.exe 报表录入及FTP自动传输系统,(多对一)初始密码为1 ReportServer.exe 报表自动接收系统,自动汇总导入数据库(DBF) SkyEditor.exe 文档录入及自动FTP发送系统 editorserver.exe 文档自动分类接收系统 SQL.exe 通用数据库查询系统 {支持各类数据库格式的查询,支持Undo 及 Redo 操作,可以保存任意中间结果 ,支持对任意结果的n次查询 支持查询结果导入excel表 支持多表查询操作} 需要 BDE skynet.exe 电子邮件系统(采用 HTTP DLL ) WebSearch.exe 文档及报表远程查询系统 webchat.exe 远程会议系统 {必须先在装有FTP SERVER 的服务器上 启动Install\Soft\New目录下ChatServer.exe} WinArchiver.exe :压缩解压系统 SkyImage.exe :图片编辑器 =====================================安装操作==================================== 在本机需要安装BDE(delphi BDE) 1: 在主机(服务器端口)上建立FTP服务(最好是微软的IIS_FTP) 设定用户名PASS 及密码USER (例如 : 用户名 :chj 密码: 1998 ),将设定的FTP用户名及密码设定为FTP的最操作权限,注意如果用微软的IIS_FTP请在操作系统用户管理器也要设定此用户名及密码并设定为隶属于Administrator,否则刚才设定的用户名及密码将无法登陆FTP SERVER; 2: 手工在本用户FTP根目录下建立以下远程文件目录: 1. dat 2. dat\regse 3. dat\regre 4. dat\update 5. report 6. report\dat01 7. report\dat02 8. report\dat03 9. report\dat04 10. report\dat05 11. report\dat06 3: 获取加密用户名及密码: 在压缩文件中找到pass.exe文件 将用户名及密码输入EDIT编辑框,点按buttom1按钮,将得到加密的用户名及密码,例如用户名 :chj 密码: 1998,得到的用户名及密码分别是 用户名 :avu 密码: 2770 4:获取主机地址加密数据,和上面一样,将运行FTP SERVER 主机地址 例如 202.87.09.91 输入EDIT编辑框点按buttom1按钮,将得到加密的主机地址 5: 执行install安装程序,系统对于普通用户及超级都用它来安装, 对于普通用户,不需要序列号,对于超级用户需要序列号,序列号为OA2000 ; 6 录入主机地址及用户密码,将加密的主机地址录入主机地址栏目中,上面的545.03.47.72,在主、副、次用户名中录入加密的FTP 用户名,例如上面的 avu 在主、副、次密码中录入加密的FTP 用户密码 例如2770 7. 填写安装区域部门选项(选择安装用户);如果选择总部各部门,比如"总经办",则密码为大写ZB+"总经办"每个字第一个大写的拼音,例如上面为ZBZJB ; 如果选择各分部,比如"安徽分部",则密码为大写 "安徽分部"每个字第一个大写的拼音,例如上面为AHFB; 如果选择各办事处,比如"安庆",则密码为大写 "安庆"每个字第一个大写的拼音,例如上面为AQ; 这些便是登陆用户信息,内容在\Install\Soft\New\NetSky\DAT 目录下,为bscid.cdx 和 zbid.cdx和fbid.cdx ;可以修改本初始文件; 可以用Delphi的ClientDataSet组件的LoadFromFile及SaveToFile方法打开本文件读写编辑,文件内容可
©️2021 CSDN 皮肤主题: 技术黑板 设计师:CSDN官方博客 返回首页
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值