PHP解决高并发问题


Posted in PHP onApril 01, 2021

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

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

PHP解决高并发问题

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

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

重启与过载保护

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

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

高并发下的数据安全

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

  1. 超发的原因

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

PHP解决高并发问题

在上面的这个图中,就导致了并发用户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. 悲观锁思路

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

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

PHP解决高并发问题

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

优化方案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,先进先出),这样的话,我们就不会导致某些请求永远获取不到锁。看到这里,是不是有点强行将多线程变成单线程的感觉哈。

PHP解决高并发问题

然后,我们现在解决了锁的问题,全部请求采用“先进先出”的队列方式来处理。那么新的问题来了,高并发的场景下,因为请求很多,很可能一瞬间将队列内存“撑爆”,然后系统又陷入到了异常状态。或者设计一个极大的内存队列,也是一种方案,但是,系统处理完一个队列内请求的速度根本无法和疯狂涌入队列中的数目相比。也就是说,队列内的请求会越积累越多,最终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的计算开销。但是,综合来说,这是一个比较好的解决方案。

PHP解决高并发问题
有很多软件和服务都“乐观锁”功能的支持,例如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;
     }
 }
 ?>
PHP 相关文章推荐
PHP无敌近乎加密方式!
Jul 17 PHP
php判断并删除空目录及空子目录的方法
Feb 11 PHP
解决PHP里大量数据循环时内存耗尽的方法
Oct 10 PHP
详解WordPress中给链接添加查询字符串的方法
Dec 18 PHP
PHP生成和获取XML格式数据的方法
Mar 04 PHP
Symfony2框架学习笔记之表单用法详解
Mar 18 PHP
JSON字符串传到后台PHP处理问题的解决方法
Jun 05 PHP
php解决和避免form表单重复提交的几种方法
Aug 31 PHP
wordpress网站转移到本地运行测试的方法
Mar 15 PHP
PHP基于ORM方式操作MySQL数据库实例
Jun 21 PHP
php处理多图上传压缩代码功能
Jun 13 PHP
laravel实现上传图片的两种方式小结
Oct 12 PHP
php引用传递
Apr 01 #PHP
PhpSpreadsheet中文文档 | Spreadsheet操作教程实例
Apr 01 #PHP
PHP判断是否是json字符串
Apr 01 #PHP
php TP5框架生成二维码链接
Apr 01 #PHP
php随机生成验证码,php随机生成数字,php随机生成数字加字母!
Apr 01 #PHP
phpQuery解析HTML乱码问题(补充官网未列出的乱码解决方案)
Apr 01 #PHP
PHP控制循环操作的时间
Apr 01 #PHP
You might like
.htaccess文件保护实例讲解
2011/02/06 PHP
让php处理图片变得简单 基于gb库的图片处理类附实例代码下载
2011/05/17 PHP
手把手编写PHP框架 深入了解MVC运行流程
2016/09/19 PHP
IE6/IE7中JavaScript json提示缺少标识符、字符串或数字问题处理
2014/12/16 Javascript
Jquery左右滑动插件之实现超级炫酷动画效果附源码下载
2015/12/02 Javascript
jquery mobile开发常见问题分析
2016/01/21 Javascript
Bootstrap 折叠(Collapse)插件用法实例详解
2016/06/01 Javascript
Node.js 日志处理模块log4js
2016/08/28 Javascript
jQuery Ajax全解析
2017/02/13 Javascript
自带气泡提示的vue校验插件(vue-verify-pop)
2017/04/07 Javascript
nodejs个人博客开发第七步 后台登陆
2017/04/12 NodeJs
详解vue2.0脚手架的webpack 配置文件分析
2017/05/27 Javascript
NodeJs使用Mysql模块实现事务处理实例
2017/05/31 NodeJs
薪资那么高的Web前端必看书单
2017/10/13 Javascript
Javascript实现异步编程的过程
2018/06/18 Javascript
示例vue 的keep-alive缓存功能的实现
2018/12/13 Javascript
vue-cli3访问public文件夹静态资源报错的解决方式
2020/09/02 Javascript
JavaScript实现浏览器网页自动滚动并点击的示例代码
2020/12/05 Javascript
原生JavaScript实现购物车
2021/01/10 Javascript
深入讲解Java编程中类的生命周期
2016/02/05 Python
Python及Django框架生成二维码的方法分析
2018/01/31 Python
Python cookbook(数据结构与算法)从序列中移除重复项且保持元素间顺序不变的方法
2018/03/13 Python
int在python中的含义以及用法
2019/06/27 Python
Keras使用tensorboard显示训练过程的实例
2020/02/15 Python
html5 canvas 画图教程案例分析
2012/11/23 HTML / CSS
Old Navy加拿大官网:美式休闲服饰品牌
2017/09/26 全球购物
6号汽车旅馆预订:Motel 6
2018/02/11 全球购物
什么是ARP(Address Resolution Protocol)地址解析协议
2013/10/31 面试题
升国旗仪式主持词
2014/03/19 职场文书
小学生综合素质评语
2014/04/23 职场文书
关于热爱祖国的演讲稿
2014/05/04 职场文书
师德演讲稿范文
2014/05/06 职场文书
2014年行政后勤工作总结
2014/12/06 职场文书
2015年考研复习计划
2015/01/19 职场文书
军训新闻稿范文
2015/07/17 职场文书
2016年父亲节寄语
2015/12/04 职场文书