使用文件锁排它锁
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等。