PHP锁定/确保给定脚本在任何时候只运行一次

10

我正在编写一段PHP脚本,希望确保在任何时候只运行一个实例。所有这些关于不同锁定方式、竞争条件等等的讨论让我感到困惑。

我不确定是采用锁文件、信号量还是使用MySQL锁等等。

有人能告诉我:

a) 实现这个的正确方法是什么?

并且

b) 指向一个PHP实现(或易于移植到PHP的东西)吗?


3
这个脚本是在网页服务器上运行还是在命令行下运行? - Marc B
如果真的很重要...网络服务器。 - Keith Palmer Jr.
1
可能是MySQL锁,如果是这样,脚本可以优雅地中止并显示“已在使用”。使用flock等会将脚本从Apache本身中锁定,并可能导致500个内部错误等问题。 - Marc B
6个回答

4
一种方法是使用php函数flock和一个虚拟文件作为看门狗。
在我们的工作开始时,如果该文件引发LOCK_EX标志,则可以退出或等待。
Php flock文档: http://php.net/manual/en/function.flock.php 对于这些示例,首先必须创建一个名为lock.txt的文件。 示例1,如果另一个同类进程正在运行,则会正确退出,而不会重试,并给出状态消息。
如果无法访问文件lock.txt,则会抛出错误状态。
<?php

$fp = fopen("lock.txt", "r+");

if (!flock($fp, LOCK_EX|LOCK_NB, $blocked)) {
    if ($blocked) {

        // another process holds the lock
        echo "Couldn't get the lock! Other script in run!\n"; 

    }
    else {
        // couldn't lock for another reason, e.g. no such file
        echo "Error! Nothing done.";
    }
}
else {
    // lock obtained
    ftruncate($fp, 0);  // truncate file

    // Your job here 
    echo "Job running!\n";
    
    // Leave a breathe
    sleep(3);

    fflush($fp);            // flush output before releasing the lock
    flock($fp, LOCK_UN);    // release the lock

}

fclose($fp); // Empty memory

示例 2FIFO(先进先出):如果有任何队列,我们希望该过程等待执行后再执行。

<?php

$fp = fopen("lock.txt", "r+");

if (flock($fp, LOCK_EX)) {  // acquire an exclusive lock
    ftruncate($fp, 0);      // truncate file

    // Your job here 
    echo "Job running!\n";

    // Leave a breathe
    sleep(3);

    fflush($fp);            // flush output before releasing the lock
    flock($fp, LOCK_UN);    // release the lock
}

fclose($fp);

可以使用fopen打开x模式的方式进行操作,当脚本结束时会创建并擦除一个文件。

创建并以只写方式打开文件;将文件指针放置于文件开头。如果文件已经存在,则fopen()调用将失败并返回FALSE。

http://php.net/manual/zh/function.fopen.php

然而,在Unix环境中,为了进行精细调整,我发现更容易使用getmypid()列出每个后台脚本的进程ID(PID)到数据库或单独的JSON文件中。

当一个任务结束时,脚本要在该文件中声明它的状态(如:成功/失败/调试信息等),然后删除它的PID。从我的角度来看,这样可以更轻松地创建管理员工具和守护程序。如果需要,可以使用posix_kill()从PHP中杀死某个PID。

微服务是使用类似于Unix管道的方式组成的。 服务可以调用服务。 https://en.wikipedia.org/wiki/Microservices

另请参阅:如何防止PHP脚本在运行时使用所有资源?


2
使用信号量:
$key = 156478953; //this should be unique for each script
$maxAcquire = 1;
$permissions =0666;
$autoRelease = 1; //releases semaphore when request is shut down (you dont have to worry about die(), exit() or return
$non_blocking = false; //if true, fails instantly if semaphore is not free

$semaphore = sem_get($key, $maxAcquire, $permissions, $autoRelease);
if (sem_acquire($semaphore, $non_blocking ))  //blocking (prevent simultaneous multiple executions)
{
    processLongCalculation();
}
sem_release($semaphore);

请见:

https://www.php.net/manual/en/function.sem-get.php

https://www.php.net/manual/en/function.sem-acquire.php

https://www.php.net/manual/en/function.sem-release.php


1
// borrow from 2 anwsers on stackoverflow
function IsProcessRunning($pid) {
    return shell_exec("ps aux | grep " . $pid . " | wc -l") > 2;
}

function AmIRunning($process_file) {
    // Check I am running from the command line
    if (PHP_SAPI != 'cli') {
        error('Run me from the command line');
        exit;
    }

    // Check if I'm already running and kill myself off if I am
    $pid_running = false;
    $pid = 0;
    if (file_exists($process_file)) {
        $data = file($process_file);
        foreach ($data as $pid) {
            $pid = (int)$pid;
            if ($pid > 0 && IsProcessRunning($pid)) {
                $pid_running = $pid;
                break;
            }
        }
    }
    if ($pid_running && $pid_running != getmypid()) {
        if (file_exists($process_file)) {
            file_put_contents($process_file, $pid);
        }
        info('I am already running as pid ' . $pid . ' so stopping now');
        return true;
    } else {
        // Make sure file has just me in it
        file_put_contents($process_file, getmypid());
        info('Written pid with id '.getmypid());
        return false;
    }
}

/*
 * Make sure there is only one instance running at a time
 */
$lockdir = '/data/lock';
$script_name = basename(__FILE__, '.php');
// The file to store our process file
$process_file = $lockdir . DS . $script_name . '.pid';

$am_i_running = AmIRunning($process_file);
if ($am_i_running) {
    exit;
}

0

如果你在Linux上使用PHP,我认为最实用的方法是:

<?php
 if(shell_exec('ps aux | grep '.__FILE__.' | wc  -l')>3){
    exit('already running...');
 }
?>

另一种方法是使用文件标志和退出回调函数,退出回调函数可以确保在任何情况下PHP执行结束或致命错误时重置文件标志为0。
<?php
function exitProcess(){
  if(file_get_contents('inprocess.txt')!='0'){
    file_put_contents('inprocess.txt','0');  
  }
}

if(file_get_contents('inprocess.txt')=='1'){
  exit();
}

file_put_contents('inprocess.txt','1');
register_shutdown_function('exitProcess');

/**
execute stuff
**/
?>


0

您可以选择适合您项目的解决方案,实现这个有两种简单的方法:文件锁定或数据库锁定。

如需了解文件锁定的具体实现,请参考 http://us2.php.net/flock

如果您已经在使用数据库,在其中创建一个表格,并为该脚本生成一个已知的令牌,执行结束后将其删除即可。为避免出现错误,您可以设置过期时间。


你确定这是一个安全的处理方式吗?PHP.net上的评论有几个条目表明它可能会引入竞态条件。 - Keith Palmer Jr.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接