php并发非阻塞处理的实现方法

本文探讨了多种并发控制及异步处理技术,包括文件锁、数据库锁、队列、Redis、FastCGI、fsockopen、cURL、Gearman、Swoole、缓存、队列、系统命令、pcntl_fork、Ajax、popen、curl和fsockopen等,旨在解决高并发场景下的资源争用问题。

使用文件锁排它锁

flock函数用于获取文件的锁,这个锁同时只能被一个线程获取到,其它没有获取到锁的线程要么阻塞,要么获取失败。在获取到锁的时候,先查询库存,如果库存大于0,则进行下订单操作,减库存,然后释放锁。

 

使用MySQL数据库提供的悲观锁

Innodb存储引擎支持行级锁,当某行数据被锁定时,其他进程不能对这行数据进行操作。

先查询并锁定行:

select stock_num from table where id=1 for update

if(stock_num > 0){

//下订单

update table set stock_num=stock-1 where id=1

}

 

使用队列

将用户的下单请求依次存入一个队列中,后台用一个单独的进程处理队列中的下单请求。

 

使用Redis

redis的操作都是原子性的,可以将商品的库存存入redis中,下单之前对库存进行decr操作,如果返回的值大于等于0等可以下单,否则不能下单,这种方式效率较高。

if(redis->get('stock_num') > 0){

 stock_num = redis->decr('stock_num')

 if(stock_num >= 0){

 //下订单

 }else{

 //库存不足

 }

}else{

//库存不足

}

 

使用 fastcgi_finish_request()

如果 PHP 与 Web 服务器使用了 PHP-FPM(FastCGI 进程管理器),那通过 fastcgi_finish_request() 函数能马上结束会话,而 PHP 线程可以继续在后台运行。

echo "program start...";

file_put_contents('log.txt','start-time:'.date('Y-m-d H:i:s'), FILE_APPEND);

fastcgi_finish_request();

sleep(1);

echo 'debug...';

file_put_contents('log.txt', 'start-proceed:'.date('Y-m-d H:i:s'), FILE_APPEND);

sleep(10);

file_put_contents('log.txt', 'end-time:'.date('Y-m-d H:i:s'), FILE_APPEND);

从输出结果可看到,页面打印完program start...,输出第一行到 log.txt 后会话就返回了,所以后面的 debug... 不会在浏览器上显示,而 log.txt 文件能完整地接收到三个完成时间。

 

使用 fsockopen()

使用 fsockopen() 打开一个网络连接或者一个Unix套接字连接,再用 stream_set_blocking() 非阻塞模式请求:

$fp = fsockopen("www.example.com", 80, $errno, $errstr, 30);

if (!$fp) {

    die('error fsockopen');

}

// 转换到非阻塞模式

stream_set_blocking($fp, 0);

$http = "GET /save.php  / HTTP/1.1\r\n";

$http .= "Host: www.example.com\r\n";

$http .= "Connection: Close\r\n\r\n";

fwrite($fp, $http);

fclose($fp);

 

使用 cURL

利用cURL中的 curl_multi_* 函数发送异步请求

$cmh = curl_multi_init();

$ch1 = curl_init();

curl_setopt($ch1, CURLOPT_URL, "http://localhost/");

curl_multi_add_handle($cmh, $ch1);

curl_multi_exec($cmh, $active);

echo "End\n";

 

使用 Gearman/Swoole 扩展

Gearman 是一个具有 php 扩展的分布式异步处理框架,能处理大批量异步任务。

Swoole 最近很火,有很多异步方法,使用简单。

 

使用缓存和队列

使用redis等缓存、队列,将数据写入缓存,使用后台计划任务实现数据异步处理。

这个方法在常见的大流量架构中应该很常见吧

 

调用系统命令

极端的情况下,可以调用系统命令,可以将数据传给后台任务执行,个人感觉不是很高效。

$cmd = 'nohup php ./processd.php $someVar >/dev/null  &';

`$cmd`

 

使用 pcntl_fork()

安装 pcntl 扩展,使用 pcntl_fork() 生成子进程异步执行任务,个人觉得是最方便的,但也容易出现僵尸进程。

$pid = pcntl_fork()

if ($pid == 0) {

    child_func();    //子进程函数,主进程运行

} else {

    father_func();   //主进程函数

}

echo "Process " . getmypid() . " get to the end.\n";

function father_func() {

    echo "Father pid is " . getmypid() . "\n";

}

function child_func() {

    sleep(6);

    echo "Child process exit pid is " . getmypid() . "\n";

    exit(0);

}

 

使用Ajax 与 img 标记

原理,服务器返回的html中插入Ajax 代码或 img 标记,img的src为需要执行的程序。
优点:实现简单,服务端无需执行任何调用
缺点:在执行期间,浏览器会一直处于loading状态,因此这种方法并不算真正的异步调用。

$.get("doRequest.php", { name: "fdipzone"} );

<img src="doRequest.php?name=fdipzone">

 

使用popen
使用popen执行命令,语法:

// popen — 打开进程文件指针 

resource popen ( string $command , string $mode )

pclose(popen('php /home/fdipzone/doRequest.php &', 'r'));

优点:执行速度快
缺点:

  • 1).只能在本机执行
  • 2).不能传递大量参数
  • 3).访问量高时会创建很多进程

 

使用curl
设置curl的超时时间 CURLOPT_TIMEOUT 为1 (最小为1),因此客户端需要等待1秒

<?php

$ch = curl_init();

$curl_opt = array(

  CURLOPT_URL, 'http://www.example.com/doRequest.php'

  CURLOPT_RETURNTRANSFER,1,

  CURLOPT_TIMEOUT,1

);

curl_setopt_array($ch, $curl_opt);

curl_exec($ch);

curl_close($ch);

 

使用fsockopen

fsockopen是最好的,缺点是需要自己拼接header部分。

<?php 

$url = 'http://www.example.com/doRequest.php';

$param = array(

  'name'=>'fdipzone',

  'gender'=>'male',

  'age'=>30

);

doRequest($url, $param);

function doRequest($url, $param=array()){

  $urlinfo = parse_url($url);

  $host = $urlinfo['host'];

  $path = $urlinfo['path'];

  $query = isset($param)? http_build_query($param) : '';

  $port = 80;

  $errno = 0;

  $errstr = '';

  $timeout = 10;

  $fp = fsockopen($host, $port, $errno, $errstr, $timeout);

  $out = "POST ".$path." HTTP/1.1\r\n";

  $out .= "host:".$host."\r\n";

  $out .= "content-length:".strlen($query)."\r\n";

  $out .= "content-type:application/x-www-form-urlencoded\r\n";

  $out .= "connection:close\r\n\r\n";

  $out .= $query;

  fputs($fp, $out);

  fclose($fp);

}

注意:当执行过程中,客户端连接断开或连接超时,都会有可能造成执行不完整,因此需要加上

ignore_user_abort(true); // 忽略客户端断开

set_time_limit(0);    // 设置执行不超时

提前结束会话(请求), 但是进程仍在执行后续耗时代码
    FastCGI模式下, 使用fastcgi_finish_request()函数能马上结束会话
    注意: fastcgi_finish_request官方介绍页面下的评论提出需要注意的点[链接](https://www.php.net/manual/zh/function.fastcgi-finish-request.php)
    正常脚本结束时php会自动调用session_write_close()函数, 而脚本在处理中的时候占用者session锁,对于后续请求来说是阻塞的.所以要尽快手动调用session_write_close()结束并保存session数据. 这对于其他有竞争锁情况同样适用,没有用了要尽快释放
    一般模式下(如Apache, Nginx, FastCGI(直接使用fastcgi_finish_request()更方便等), 提前输出内容, 结束会话     

<?php
      //适用于大多数运行模式(不包括命令行模式)
      set_time_limit(0);  //设置不限执行时间
      ignore_user_abort(true);  //忽略客户端中断
      //nginx等可能需要达到4k才会输出buffer,所有先输出一些空字符串
      $str = str_repeat(' ', 65536);
      $str .= '立即输出' . date('Y-m-d H:i:s');
      #header('X-Accel-Buffering: no');   // 关闭加速缓冲, 在nginx模式需要开启此行
      header("Content-Type: text/html;charset=utf-8");
      header("Connection: close");//告诉浏览器不需要保持长连接
      header('Content-Length: '. strlen($str));//告诉浏览器本次响应的数据大小只有上面的echo那么多
      ob_end_flush();
      ob_start();
      echo $str;
      ob_flush();
      flush();
      //至此,连接已经关闭. 但是进程还不会结束, 以下程序还能运行但不会输出
      sleep(10);
      file_put_contents('./log.txt', '10s后我写入log文本: 时间' . date('Y-m-d H:i:s'));

      注意: 在以下情况中,该方法失效:无论那个模式,gzip一定要关闭; 是window32下web服务不行;   [官方说明](https://www.php.net/manual/zh/function.flush.php)      
        个别web服务器程序,特别是Win32下的web服务器程序,在发送结果到浏览器之前,仍然会缓存脚本的输出,直到程序结束为止。        
        有些Apache的模块,比如mod_gzip,可能自己进行输出缓存,这将导致flush()函数产生的结果不会立即被发送到客户端浏览器。
     
开启子进程(确保以下函数没有被禁用)
    popen 
      链接https://www.php.net/manual/zh/function.popen.php,打开一个指向进程的管道,该进程由派生给定的command命令执行而产生。他是单向的(只能用于写或读) ,不等待子进程返回结果直接结束父脚本

<?php
      ini_set('date.timezone', 'Asia/shanghai');
      echo "父脚本开始\n";
      echo date('Y-m-d H:i:s') . "\n";
      //&: 转入后台运行; nohup:不挂断地运行命令, 防止当前的终端窗口被关闭后导致进程结束
      $cmd = 'nohup php cmd.php &';
      pclose(popen($cmd, 'r'));//开启一个子进程后马上关闭, 子进程进入后台处理耗时的处理
      echo "父脚本结束";
       
      - 例子2 (等待子进程返回结果在结束父脚本)
      <?php
      ini_set('date.timezone', 'Asia/shanghai');
      echo "父进程开始\n";
      echo date('Y-m-d H:i:s') . "\n";
      $cmd1 = 'nohup php cmd.php &';
      $cmd2 = 'nohup php cmd1.php &';     
      //执行耗时异步任务1
      $p1 = popen($cmd1, 'r');
      //执行耗时异步任务2, 与任务1是并行运行的
      $p2 = popen($cmd2, 'r');
      register_shutdown_function(function () use ($p1, $p2) {
          $res1 = stream_get_contents($p1);
          echo $res1;
          $res2 = stream_get_contents($p2);
          echo $res2;
          pclose($p1);
          pclose($p2);
          echo "父脚本结束" . date('Y-m-d H:i:s') . "\n";
      });

     proc_open 开启异步子进程
     与popen()一样, 只是该函数有更强的控制程序执行的能力, 可以双向(读又写)
     参考例子(https://my.oschina.net/eechen/blog/745504)
     pcntl_fork  需要扩展支持,比较麻烦- 4.借助框架如swoole等。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值