如何在一段时间内重试 PHP flock()


How do I retry a PHP flock() for a period of time?

我需要打开一个日志文件进行写入。 问题是,很多事情可能会同时这样做,我不希望冲突。 每次写入将是一行,通常约为 150 字节(并且始终小于 1K),并且不严格要求按时间顺序获取内容。

我想我想要的是尝试flock(),如果失败了,请继续尝试几秒钟。 如果多次尝试后仍无法建立锁定,请放弃。

$fh=fopen($logfile, "a");
if (flock($fh, LOCK_EX|LOCK_NB)) {
  $locked=TRUE;
} else {
  $locked=FALSE;
  // Retry lock every 0.1 seconds for 3 seconds...
  $x=0; while($x++ < 30) {
    usleep(100000);
    if (flock($fh, LOCK_EX|LOCK_NB)) {
      $locked=TRUE;
      break;
    }
  }
}
if ($locked) {
  if (fwrite($fh, strftime("[%Y-%m-%d %T] ") . $logdata . "'n")) {
    print "Success.'n";
  } else {
    print "Fail.'n";
  }
  flock($fh, LOCK_UN)
} else {
  print "Lock failed.'n";
}

我有两个问题,一个是一般性问题,一个是具体问题。 首先,除了以不同的方式(do...while等)实现相同的解决方案之外,是否有更好的通用策略来处理此类仅在PHP中运行的问题? 其次,有没有更好的方法在PHP中实现这一点? (是的,我把它们分开了,因为我对策略部分真的很感兴趣。

我考虑过的一种选择是使用 syslog(),但 PHP 代码可能需要在系统级管理(即向/etc/syslog.conf 添加内容)可能无法作为选项的平台上运行。

更新:根据兰迪的建议,在上面的代码中添加了|LOCK_NB

根据我在 PHP 制作日志方面的长期经验(在 Linux 下!我从未遇到过冲突问题(即使有数百个同时写入和并发写入)。如此简单的跳过锁定管理:

$fh=fopen($logfile, "a");
if (fwrite($fh, strftime("[%Y-%m-%d %T] ") . $logdata . "'n")) {
    print "Success.'n";
  } else {
    print "Fail.'n";
  }
fclose($fh);

关于此策略,文件日志记录(带或不带锁)不是最佳解决方案,因为每个带有"a"的 fopen 都意味着一个搜索系统调用,用于在文件末尾设置光标。保持文件打开的系统日志可避免此开销。

当然,对于"大"文件,开销变得很重要(在性能上),一个简单的解决方案是创建名称中包含日期(或日期时间)的日志文件。

Apache 软件包包含一个测试程序:AB,允许并发查询,您可以测试我的论文,强调您的服务器由 100000 个线程完成 1000000 个查询。

添加 - 在评论之后

不,这不是一项不可能完成的任务。

我找到了一张来自 http://php.net/manual/en/function.fwrite.php 的便笺

如果句柄在追加模式下是 fopen()ed,则 fwrite() 是原子的(除非 字符串的大小超过了文件系统的块大小,在某些 平台,只要文件位于本地文件系统上)。那是 在调用 fwrite() 之前不需要 flock() 资源;全部 数据将不间断地写入。

要知道一个块有多大(以字节为单位)(通常为 4K):

dumpe2fs/dev/sd_your_disk_partition |less -i

写入的"

原子性"是实现的,阻止其他"代理"写入(当你在"ps ax"中看到进程处于"D"状态时),但是PHP流工具可以解决这个问题,参见:*stream_set_blocking*。此方法可能会引入部分写入,因此您必须验证记录的完整性。

在任何情况下,无论使用鸡,fwrite(网络或文件)都容易受到阻塞/故障的影响。恕我直言,羊群只引入开销。

关于您最初的问题和您的目标(尝试在高度规避风险的环境中实施公司政策),即使是 fwrite 也可能出现问题,我只能想象一个简单的解决方案:使用 DB

  • 大部分复杂性都来自 PHP,是用 C 编写的!
  • 完全控制操作流程
  • 高并发级别

这是我最近在 https://CodeReview.stackexchange.com/questions/283930/php-flock-with-timeout-for-lock-sh-lock-ex 发布的flockWithTimeout的实现

function flockWithTimeout($handle, int $flags, int &$would_block = null, float $timeout_seconds = 10, float $sleep_time_seconds = 0.01): bool
{
    if($flags !== LOCK_UN) {
        $flags = $flags | LOCK_NB;
    }
    $would_block = null;
    $timeout_timestamp = microtime(true) + $timeout_seconds;
    $sleep_time_microseconds = (int)($sleep_time_seconds * 1000000);
    for (;;) {
        $success = flock($handle, $flags, $would_block);
        if ($success) {
            return true;
        }
        if (!$would_block) {
            // something else is wrong, like flocking on FAT32 which doesn't support flock?
            return false;
        }
        // another process has the lock
        if (microtime(true) >= $timeout_timestamp) {
            return false;
        }
        usleep($sleep_time_microseconds);
    }
    throw new 'LogicException('unreachable');
}

示例用法:

$h = tmpfile();
$h2 = fopen(stream_get_meta_data($h)['uri'], "rb");
if(flockWithTimeout($h, LOCK_EX)){
    echo "got the lock! :)'n";
} else {
    echo "did not get the lock :('n";
}
$timeout_seconds = 0.1;
if(flockWithTimeout($h2, LOCK_SH, $would_block, $timeout_seconds)){
    echo "got the lock! :)'n";
} else {
    echo "did not get the lock :('n";
}

应该打印

got the lock! :)
did not get the lock :(

did not get the lock :(大约需要 100 毫秒