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

PHP flock() for read-modify-write does not work

我有一个由 PHP 脚本维护的日志文件。 PHP 脚本受并行处理。我无法使用 flock() 机制来处理日志文件:在我的例子中,flock() 不会阻止 PHP 脚本 运行 并行共享的日志文件被访问同时,有时会被覆盖。

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

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

有任何建议可以使这项工作或替代方法吗?

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

这里有一些独立的 运行 演示代码来说明。它生成多个从浏览器到服务器的并行请求并显示结果。从视觉上很容易观察到一个故障:如果您的网络服务器不像我的服务器那样支持 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
系统:Linuxcluster026.gra.hosting.ovh.net
服务器 API:CGI/FastCGI

使用仅由 PHP 请求处理程序协调的文件进行数据管理,您正走向一个痛苦的世界 - 到目前为止,您只是刚刚试水。

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

更复杂的是您只能在 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);
 }

(请注意,这是一个非常丑陋的 hack)

在 PHP 中进行原子测试和设置指令的一种方法是使用 mkdir()。使用目录而不是文件有点奇怪,但是 mkdir() 将创建一个目录或 return 如果它已经存在则为 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);

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

x Create and open for writing only; place the file pointer at the beginning of the file. If the file already exists, the fopen() call will fail by returning FALSE and generating an error of level E_WARNING. If the file does not exist, attempt to create it.

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);

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