异步运行PHP任务

dav*_*avr 136 php queue asynchronous background task

我在一个有点大的Web应用程序上工作,后端主要是在PHP中.代码中有几个地方我需要完成一些任务,但我不想让用户等待结果.例如,在创建新帐户时,我需要向他们发送欢迎电子邮件.但是当他们点击"完成注册"按钮时,我不想让他们等到实际发送电子邮件,我只想启动该过程,并立即向用户返回消息.

到目前为止,在某些地方,我一直在使用exec()感觉像是一个黑客.基本上做的事情如下:

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");
Run Code Online (Sandbox Code Playgroud)

这似乎有效,但我想知道是否有更好的方法.我正在考虑编写一个在MySQL表中排队任务的系统,以及一个单独的长时间运行的PHP脚本,每秒查询一次该表,并执行它找到的任何新任务.如果需要的话,这也有可能让我将来在几台工作机器之间拆分任务.

我是在重新发明轮子吗?有没有比exec()hack或MySQL队列更好的解决方案?

Pau*_*xon 76

我已经使用了排队方法,并且它可以正常工作,因为您可以推迟处理,直到您的服务器负载空闲,如果您可以轻松地分割"非紧急的任务",则可以非常有效地管理负载.

滚动你自己并不是太棘手,这里还有一些其他的选择:

  • GearMan - 这个答案写于2009年,从那时起GearMan看起来很受欢迎,请参阅下面的评论.
  • ActiveMQ如果你想要一个完整的开源消息队列.
  • ZeroMQ - 这是一个非常酷的套接字库,可以轻松编写分布式代码,而无需过多担心套接字编程本身.您可以将它用于单个主机上的消息队列 - 您只需让您的webapp将某些内容推送到一个连续运行的控制台应用程序将在下一个合适的机会消耗的队列中
  • beanstalkd - 在写这个答案时才发现这个,但看起来很有趣
  • dropr是一个基于PHP的消息队列项目,但自2010年9月以来一直没有得到积极维护
  • php-enqueue是最近(2017)维护的各种队列系统包装器
  • 最后,关于使用memcached进行消息排队的博客文章

另一个也许更简单的方法是使用ignore_user_abort - 一旦你将页面发送给用户,你就可以进行最后的处理而不用担心提前终止,尽管这会产生延长用户页面负载的效果.透视.

  • 如果在"感谢注册"响应中设置Content-Length HTTP标头,则浏览器应在收到指定的字节数后关闭连接.这使服务器端进程运行(假设设置了ignore_user_abort),而不让最终用户等待.当然,在渲染标题之前,您需要计算响应内容的大小,但这对于简短的响应来说非常容易. (2认同)

小智 22

当您只想执行一个或多个HTTP请求而不必等待响应时,还有一个简单的PHP解决方案.

在调用脚本中:

$socketcon = fsockopen($host, 80, $errno, $errstr, 10);
if($socketcon) {   
   $socketdata = "GET $remote_house/script.php?parameters=... HTTP 1.1\r\nHost: $host\r\nConnection: Close\r\n\r\n";      
   fwrite($socketcon, $socketdata); 
   fclose($socketcon);
}
// repeat this with different parameters as often as you like
Run Code Online (Sandbox Code Playgroud)

在被调用的script.php上,您可以在第一行中调用这些PHP函数:

ignore_user_abort(true);
set_time_limit(0);
Run Code Online (Sandbox Code Playgroud)

这会导致脚本在HTTP连接关闭时无时间限制地继续运行.


roj*_*oca 17

fork进程的另一种方法是通过curl.您可以将内部任务设置为Web服务.例如:

然后在您的用户访问的脚本中调用该服务:

$service->addTask('t1', $data); // post data to URL via curl
Run Code Online (Sandbox Code Playgroud)

您的服务可以使用mysql或您喜欢的任何内容跟踪任务队列:它全部包含在服务中,而您的脚本只是在使用URL.如果需要,这可以让您将服务移动到另一台机器/服务器(即可轻松扩展).

添加http授权或自定义授权方案(如亚马逊的Web服务)可让您打开由其他人/服务(如果需要)使用的任务,并且您可以进一步使用并在顶部添加监控服务以跟踪队列和任务状态.

它确实需要一些设置工作,但有很多好处.

  • 我不喜欢这种方法,因为它使网络服务器过载 (2认同)

Ali*_*man 7

我已经将Beanstalkd用于一个项目,并计划再次使用.我发现它是运行异步进程的绝佳方式.

我用它完成的一些事情是:

  • 图像大小调整 - 并且轻载队列传递给基于CLI的PHP脚本,调整大(2mb +)图像的大小工作正常,但尝试在mod_php实例中调整相同图像的大小经常遇到内存空间问题(I将PHP进程限制为32MB,调整大小超过了
  • 近期检查 - beanstalkd有可用的延迟(让这项工作只能在X秒后运行) - 所以我可以在事件发生后5到10次检查,稍晚一点

我编写了一个基于Zend-Framework的系统来解码一个"漂亮的"url,例如,调整它调用的图像的大小QueueTask('/image/resize/filename/example.jpg').首先将URL解码为数组(模块,控制器,操作,参数),然后转换为JSON以注入队列本身.

一个长时间运行的cli脚本然后从队列中获取作业,运行它(通过Zend_Router_Simple),如果需要,将信息放入memcached中,以便PHP完成时根据需要获取网站.

我还提到的一个问题是cli-script在重新启动之前只运行了50个循环,但是如果它确实想要按计划重启,它会立即执行(通过bash脚本运行).如果出现问题并且我做了exit(0)(默认值为exit;die();),它将首先暂停几秒钟.


Oma*_* S. 7

PHP具有多线程,只是默认情况下未启用,有一个名为pthreads的扩展可以做到这一点。不过,您需要使用 ZTS 编译 php。(线程安全)链接:

例子

另一个教程

pthreads PECL 扩展

更新:自从 PHP 7.2 并行扩展开始发挥作用

教程/示例

参考手册

  • 现在已经过时,被并行取代。 (3认同)

小智 7

如果只是提供昂贵任务的问题,如果支持php-fpm,为什么不使用fastcgi_finish_request()函数?

此函数将所有响应数据刷新到客户端并完成请求.这允许执行耗时的任务而不会断开与客户端的连接.

你并没有真正以这种方式使用异步性:

  1. 首先制作所有主要代码.
  2. 执行fastcgi_finish_request().
  3. 做所有沉重的东西.

再次需要php-fpm.


And*_*ore 5

这是我为我的Web应用程序编写的一个简单类.它允许分支PHP脚本和其他脚本.适用于UNIX和Windows.

class BackgroundProcess {
    static function open($exec, $cwd = null) {
        if (!is_string($cwd)) {
            $cwd = @getcwd();
        }

        @chdir($cwd);

        if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
            $WshShell = new COM("WScript.Shell");
            $WshShell->CurrentDirectory = str_replace('/', '\\', $cwd);
            $WshShell->Run($exec, 0, false);
        } else {
            exec($exec . " > /dev/null 2>&1 &");
        }
    }

    static function fork($phpScript, $phpExec = null) {
        $cwd = dirname($phpScript);

        @putenv("PHP_FORCECLI=true");

        if (!is_string($phpExec) || !file_exists($phpExec)) {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', dirname(ini_get('extension_dir'))) . '\php.exe';

                if (@file_exists($phpExec)) {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            } else {
                $phpExec = exec("which php-cli");

                if ($phpExec[0] != '/') {
                    $phpExec = exec("which php");
                }

                if ($phpExec[0] == '/') {
                    BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
                }
            }
        } else {
            if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
                $phpExec = str_replace('/', '\\', $phpExec);
            }

            BackgroundProcess::open(escapeshellarg($phpExec) . " " . escapeshellarg($phpScript), $cwd);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)