用于读取-修改-写入的PHP flock()不起作用

时间:2018-07-03 13:27:46

标签: php mutex file-locking flock test-and-set

我有一个由PHP脚本维护的日志文件。 PHP脚本需要进行并行处理。我无法使用flock()机制来处理日志文件:就我而言,flock()不能防止同时运行并行运行的PHP脚本共享的日志文件,并且有时会覆盖它们

我希望能够读取文件,进行一些处理,修改数据并写回,而无需在服务器上同时运行相同的代码。读取修改写入必须按顺序进行。

在我的一个共享主机上(法国OVH),它无法按预期工作。在那种情况下,我们看到计数器$c在不同的iframe中具有相同的值,如果锁按预期方式工作(在其他共享主机上也是如此),则应该是不可能的。

有什么建议可以使这项工作奏效,或者有其他替代方法?

Google搜索"read modify write" phpfetch and addtest and set并没有提供有用的信息:所有解决方案都基于有效的flock()。

这里有一些独立运行的演示代码来说明。它从浏览器到服务器生成许多并行请求,并显示结果。很容易从视觉上观察到故障:如果您的Web服务器不像我的服务器那样支持flock(),则计数器值和日志行数在某些帧中将是相同的。

<!DOCTYPE html>
<html lang="en">
<title>File lock test</title>
<style>
iframe {
    width: 10em;
    height: 300px;
}
</style>
<?php
$timeStart = microtime(true);
if ($_GET) { // iframe
    // GET
    $time = $_GET['time'] ?? 'no time';
    $instance = $_GET['instance'] ?? 'no instance';

    // open file
    // $mode = 'w+'; // no read
    // $mode = 'r+'; // does not create file, we have to lock file creation also
    $mode = 'c+'; // read, write, create
    $fhandle = fopen(__FILE__ .'.rwtestfile.txt', $mode) or exit('fopen');
    // lock
    flock($fhandle, LOCK_EX) or exit('flock');
    // start of file (optional, only some modes like require it)
    rewind($fhandle);
    // read file (or default initial value if new file)
    $fcontent = fread($fhandle, 10000) or ' 0';
    // counter value from previous write is last integer value of file
    $c = strrchr($fcontent, ' ') + 1;
    // new line for file
    $fcontent .= "<br />\n$time $instance $c";
    // reset once in a while
    if ($c > 20) {
        $fcontent = ' 0'; // avoid long content
    }
    // simulate other activity
    usleep(rand(1000, 2000));
    // start of file
    rewind($fhandle);
    // write
    fwrite($fhandle, $fcontent) or exit('fwrite');
    // truncate (in unexpected case file is shorter now)
    ftruncate($fhandle, ftell($fhandle)) or exit('ftruncate');
    // close
    fclose($fhandle) or exit('fclose');
    // echo
    echo "instance:$instance c:$c<br />";
    echo $timeStart ."<br />";
    echo microtime(true) - $timeStart ."<br />";
    echo $fcontent ."<br />";
} else {
    echo 'File lock test<br />';
    // iframes that will be requested in parallel, to check flock
    for ($i = 0; $i < 14; $i++) {
        echo '<iframe src="?instance='. $i .'&time='. date('H:i:s') .'"></iframe>'."\n";
    }
}

PHP: flock - Manual中有关于flock()限制的警告,但这是关于ISAPI(Windows)和FAT(Windows)的警告。我的服务器配置是:
PHP版本7.2.5
系统: Linux cluster026.gra.hosting.ovh.net
服务器API:CGI / FastCGI

3 个答案:

答案 0 :(得分:0)

使用仅由PHP请求处理程序协调的文件进行数据管理,您将要经历一个痛苦的世界-到目前为止,您只是将脚趾浸入水中。

使用LOCK_EX,您的编写器需要等待LOCK_SH的任何(和每个)实例被释放,然后才能获取锁。在这里,您将flock设置为阻塞,直到可以获取锁为止。在相对繁忙的系统上,可能会无限期地阻止作者。在大多数操作系统上,没有优先级的排队队列,可以将随后的所有请求锁定的读取器置于等待写锁定的进程之后。

更复杂的是,您只能在 open 文件句柄上使用flock。这意味着打开文件并获取锁不是原子的,此外,您还需要刷新状态缓存,以便在获取锁后确定文件的生存期。

任何对文件的写操作(即使使用file_put_contents())也不是原子的。因此,在没有排他锁定的情况下,您无法确定没有人会读取部分文件。

如果没有其他组件(例如,提供锁排队机制的守护程序,Web服务器前的缓存反向代理或关系数据库),那么您唯一的选择是假设您无法确保独占访问和使用原子操作来信号化文件,例如:

 $lock_age=time()-filectime(dirname(CACHE_FILE) . "/lock");
 if (filemtime(CACHE_FILE)>time()-CACHE_TTL 
       && $lock_age>MAX_LOCK_TIME) {
          rmdir(dirname(CACHE_FILE) . "/lock");
          mkdir(dirname(CACHE_FILE) . "/lock") || die "I give up";
      }
      $content=generate_content(); // might want to add specific timing checks around this
      file_put_contents(CACHE_FILE, $content);
      rmdir(dirname(CACHE_FILE) . "/lock");
 } else if (is_dir(dirname(CACHE_FILE) . "/lock") {
      $snooze=MAX_LOCK_TIME-$lock_age;
      sleep($snooze);
      $content=file_get_contents(CACHE_FILE);
 } else {
      $content=file_get_contents(CACHE_FILE);
 }

(请注意,这确实很丑陋)

答案 1 :(得分:0)

在PHP中执行原子测试和设置指令的一种方法是使用mkdir()。使用目录而不是文件有点奇怪,但是mkdir()将创建目录,或者如果目录已经存在,则返回false(以及抑制警告)。 fopen()fwrite()file_put_contents()之类的文件命令不会在一条指令中进行测试和设置。

<?php
// lock
$fnLock = __FILE__ .'.lock'; // lock directory filename
$lockLooping = 0; // counter can be used for tuning depending on lock duration
do {
    if (@mkdir($fnLock, 0777)) { // mkdir is a test and set command
        $lockLooping = 0;
    } else {
        $lockLooping += 1;
        $lockAge = time() - filemtime($fnLock);
        if ($lockAge > 10) {
            rmdir($fnLock); // robustness, in case a lock was not erased                
        } else {
            // wait without consuming CPU before try again
            usleep(rand(2500, 25000)); // random to avoid parallel process conflict again
        }
    }
} while ($lockLooping > 0);

// do stuff under atomic protection
// don't take too long, because parallel processes are waiting for the unlock (rmdir)

$content = file_get_contents($protected_file_name);  // example read
$content = $modified_content; // example modify
file_put_contents($protected_file_name, $modified_content); // example write

// unlock
rmdir($fnLock);

答案 2 :(得分:0)

有一种fopen()测试和设置模式:x模式。

  

x创建并打开以仅用于写作;将文件指针放在文件的开头。如果文件已存在,则fopen()调用将失败,返回FALSE并生成错误级别E_WARNING。如果该文件不存在,请尝试创建它。

fopen($filename ,'x')的行为与mkdir()相同,并且可以以相同的方式使用:

<?php
// lock
$fnLock = __FILE__ .'.lock'; // lock file filename
$lockLooping = 0; // counter can be used for tuning depending on lock duration
do {
    if ($lockHandle = @fopen($fnLock, 'x')) { // test and set command
        $lockLooping = 0;
    } else {
        $lockLooping += 1;
        $lockAge = time() - filemtime($fnLock);
        if ($lockAge > 10) {
            rmdir($fnLock); // robustness, in case a lock was not erased                
        } else {
            // wait without consuming CPU before try again
            usleep(rand(2500, 25000)); // random to avoid parallel process conflict again
        }
    }
} while ($lockLooping > 0);

// do stuff under atomic protection
// don't take too long, because parallel processes are waiting for the unlock (rmdir)

$content = file_get_contents($protected_file_name);  // example read
$content = $modified_content; // example modify
file_put_contents($protected_file_name, $modified_content); // example write

// unlock
fclose($lockHandle);
unlink($fnLock);

最好对此进行测试,例如使用问题中的代码。 许多人依赖于文档中记载的锁定,但是在负载下的测试或生产过程中可能会出现意外情况(来自一个浏览器的并行请求可能就足够了。)