异步运行PHP任务

时间:2009-05-13 16:12:47

标签: php asynchronous background queue task

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

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

exec("doTask.php $arg1 $arg2 $arg3 >/dev/null 2>&1 &");

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

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

15 个答案:

答案 0 :(得分:77)

我已经使用了排队方法,并且它运行良好,因为您可以推迟处理,直到您的服务器负载空闲为止,如果您可以轻松地分割“非紧急的任务”,则可以非常有效地管理负载。

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

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

另一个也许更简单的方法是使用ignore_user_abort - 一旦你将页面发送给用户,你就可以进行最后的处理而不用担心提前终止,尽管这确实会产生影响。从用户角度延长页面加载量。

答案 1 :(得分:20)

当您只想执行一个或多个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

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

ignore_user_abort(true);
set_time_limit(0);

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

答案 2 :(得分:17)

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

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

$service->addTask('t1', $data); // post data to URL via curl

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

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

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

答案 3 :(得分:7)

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

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

  • 图像大小调整 - 并且轻载队列传递给基于CLI的PHP脚本,调整大(2mb +)图像的大小工作得很好,但是尝试在mod_php实例中调整相同图像的大小会定期运行到内存空间问题(我将PHP进程限制为32MB,调整大小不止于此)
  • 近期检查 - beanstalkd有可用的延迟(让这项工作只能在X秒后运行) - 所以我可以在一段时间后启动5或10次检查

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

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

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

答案 4 :(得分:7)

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

  

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

你不会以这种方式真正使用异步性:

  1. 首先制作所有主要代码。
  2. 执行fastcgi_finish_request()
  3. 制作所有重物。
  4. 再次需要php-fpm。

答案 5 :(得分: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);
        }
    }
}

答案 6 :(得分:4)

这与我几年来使用的方法相同,我没有看到或发现任何更好的方法。正如人们所说,PHP是单线程的,所以你没有其他的东西可以做。

我实际上已经添加了一个额外的级别,并且正在获取并存储进程ID。这允许我重定向到另一个页面并让用户坐在该页面上,使用AJAX检查进程是否完成(进程ID不再存在)。这对于脚本长度会导致浏览器超时的情况很有用,但用户需要等到该脚本在下一步之前完成。 (在我的情况下,它正在处理大型ZIP文件,其中包含CSV文件,这些文件可以向数据库添加多达30 000条记录,之后用户需要确认一些信息。)

我还使用了类似的报告生成过程。我不确定我会对电子邮件之类的东西使用“后台处理”,除非SMTP存在真正的问题。相反,我可能会使用一个表作为队列,然后有一个每分钟运行一次的进程来发送队列中的电子邮件。您需要谨慎地发送两次电子邮件或其他类似问题。我也会考虑对其他任务进行类似的排队过程。

答案 7 :(得分:3)

PHP HAS 多线程,默认情况下它没有启用,有一个名为 pthreads 的扩展名就是这样做的。 你需要用ZTS编译的PHP。 (线程安全) 链接:

Examples

Another tutorial

pthreads PECL Extension

答案 8 :(得分:2)

按照rojoca的建议使用cURL是一个好主意。

这是一个例子。您可以在脚本在后台运行时监视text.txt:

<?php

function doCurl($begin)
{
    echo "Do curl<br />\n";
    $url = 'http://'.$_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
    $url = preg_replace('/\?.*/', '', $url);
    $url .= '?begin='.$begin;
    echo 'URL: '.$url.'<br>';
    $ch = curl_init();
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    $result = curl_exec($ch);
    echo 'Result: '.$result.'<br>';
    curl_close($ch);
}


if (empty($_GET['begin'])) {
    doCurl(1);
}
else {
    while (ob_get_level())
        ob_end_clean();
    header('Connection: close');
    ignore_user_abort();
    ob_start();
    echo 'Connection Closed';
    $size = ob_get_length();
    header("Content-Length: $size");
    ob_end_flush();
    flush();

    $begin = $_GET['begin'];
    $fp = fopen("text.txt", "w");
    fprintf($fp, "begin: %d\n", $begin);
    for ($i = 0; $i < 15; $i++) {
        sleep(1);
        fprintf($fp, "i: %d\n", $i);
    }
    fclose($fp);
    if ($begin < 10)
        doCurl($begin + 1);
}

?>

答案 9 :(得分:1)

不幸的是,PHP没有任何类型的本机线程功能。所以我认为在这种情况下你别无选择,只能使用某种自定义代码来做你想做的事。

如果你在网上搜索PHP线程的东西,有些人已经想出了在PHP上模拟线程的方法。

答案 10 :(得分:1)

如果在“Thank You For Registering”响应中设置Content-Length HTTP标头,则浏览器应在收到指定的字节数后关闭连接。这使服务器端进程保持运行(假设设置了ignore_user_abort),这样它就可以在不让最终用户等待的情况下完成工作。

当然,您需要在呈现标题之前计算响应内容的大小,但这对于简短响应(将输出写入字符串,调用strlen(),调用header(),呈现字符串)非常容易。 / p>

这种方法的优点是强迫您管理“前端”队列,虽然您可能需要在后端做一些工作以防止竞争HTTP子进程踩到无论如何,这是你需要做的事情。

答案 11 :(得分:1)

如果您不想要完整的ActiveMQ,我建议您考虑RabbitMQ。 RabbitMQ是使用AMQP standard的轻量级消息传递。

我建议同时查看php-amqplib - 一个流行的AMQP客户端库,以访问基于AMQP的消息代理。

答案 12 :(得分:0)

我认为您应该尝试这种技术,这将有助于调用您喜欢的页面,所有页面将立即独立运行,而无需等待每个页面响应异步。

cornjobpage.php //主页

    <?php

post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue");
//post_async("http://localhost/projectname/testpage.php", "Keywordname=testValue2");
//post_async("http://localhost/projectname/otherpage.php", "Keywordname=anyValue");
//call as many as pages you like all pages will run at once independently without waiting for each page response as asynchronous.
            ?>
            <?php

            /*
             * Executes a PHP page asynchronously so the current page does not have to wait for it to     finish running.
             *  
             */
            function post_async($url,$params)
            {

                $post_string = $params;

                $parts=parse_url($url);

                $fp = fsockopen($parts['host'],
                    isset($parts['port'])?$parts['port']:80,
                    $errno, $errstr, 30);

                $out = "GET ".$parts['path']."?$post_string"." HTTP/1.1\r\n";//you can use POST instead of GET if you like
                $out.= "Host: ".$parts['host']."\r\n";
                $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
                $out.= "Content-Length: ".strlen($post_string)."\r\n";
                $out.= "Connection: Close\r\n\r\n";
                fwrite($fp, $out);
                fclose($fp);
            }
            ?>

<强> testpage.php

    <?
    echo $_REQUEST["Keywordname"];//case1 Output > testValue
    ?>

PS:如果您想将url参数作为循环发送,请遵循以下答案:https://stackoverflow.com/a/41225209/6295712

答案 13 :(得分:0)

使用exec()在服务器上生成新进程或使用curl直接在另一台服务器上生成新进程并不能完全扩展所有进程,如果我们去执行exec,您基本上是在运行长时间运行的进程可以由其他非面向Web的服务器处理,并使用curl绑定另一台服务器,除非您构建某种负载平衡。

我在一些情况下使用过Gearman,我觉得这种用例更好。我可以使用单个作业队列服务器基本上处理服务器需要完成的所有作业的排队并启动工作服务器,每个工作服务器可以根据需要运行多个工作进程实例,并扩展数量工作服务器根据需要在不需要时将其旋转。它还允许我在需要时完全关闭工作流程并将工作排队,直到工人重新上线为止。

答案 14 :(得分:-4)

PHP是一种单线程语言,因此除了使用execpopen之外,没有官方方法可以使用它来启动异步过程。有一篇关于here的博客文章。您对MySQL队列的想法也是一个好主意。

此处的具体要求是向用户发送电子邮件。我很好奇为什么你试图异步这样做,因为发送电子邮件是一项非常简单快速的任务。我想如果你发送大量的电子邮件而你的ISP因涉嫌发送垃圾邮件而阻止你,这可能是排队的一个原因,但除此之外我无法想到有任何理由这样做。