当在 PHP 7.4 中使用 file_put_contents() 时,读取文件内容为空的排查分析

1、查看 Illuminate\Filesystem\Filesystem 中的 put() 方法实现

    public function put($path, $contents, $lock = false)
    {
        return file_put_contents($path, $contents, $lock ? LOCK_EX : 0);
    }

2、参考:即在并发场景中,如果您不将其包装如下,则 file_get_contents 可能会返回空 https://www.php.net/manual/zh/function.file-put-contents.php#112831 。重要的是要理解 LOCK_EX 不会阻止读取文件,除非您还使用 PHP ‘flock’ 函数显式获取读锁(共享锁定)。即在并发场景中,如果您不将其包装如下,则 file_get_contents 可能会返回空。如果您有代码对文件执行 file_get_contents,更改字符串,然后使用 file_put_contents 重新保存,您最好确保正确执行此操作,否则您的文件将随机擦除自身。

<?php
$myfile=fopen('test.txt','rt');
flock($myfile,LOCK_SH);
$read=file_get_contents('test.txt');
fclose($myfile);
?>

3、决定在测试环境中模拟出使用 file_put_contents() 同时写入同一路径文件的情况,然后观察是否会出现读取文件内容为空的情况。

require.php

<?php

$i = rand();
$path = 'return.php';
$contents = [
 'a',
 'b',
 'c',
 'd',
 'e',
 'f',
 'g',
 'h',
 'i',
 'j'
];

if (file_exists($path)) {
 $read = file_get_contents($path);
 if (empty($read)) {
  file_put_contents('empty-' . rand() . '.php', $read, LOCK_EX);
 }
}
$code = '<?php';
$code .= "\n\n";
$contents = array_merge($contents, [$i]);
$code .= 'return ' . var_export($contents, true) . ';';
file_put_contents($path, $code, LOCK_EX);

?>

4、参考:使用阿里云 性能测试 PTS 模拟并发 http 请求 。 查看程序运行结果,确定生成了 2万多 个文件,分别为:return.php、以及大量以 empty- 开头的文件。如图1

图1

5、由此得出结论,如果您有代码对文件内容使用 file_put_contents 进行保存,虽然已经添加 LOCK_EX,但是在高并发场景中,其在读取文件内容时,是有很大的概率读取到文件内容为空的情况的。

6、决定在读取文件内容前,先使用 PHP ‘flock’ 函数显式获取读锁(共享锁定)。最终代码实现如下

require.php

<?php

$i = rand();
$path = 'return.php';
$contents = [
 'a',
 'b',
 'c',
 'd',
 'e',
 'f',
 'g',
 'h',
 'i',
 'j'
];

if (file_exists($path)) {
 $myfile = fopen($path, 'r');
 flock($myfile, LOCK_SH);
 $read = file_get_contents($path);
 fclose($myfile);
 if (empty($read)) {
  file_put_contents('empty-' . rand() . '.php', $read, LOCK_EX);
 }
}
$code = '<?php';
$code .= "\n\n";
$contents = array_merge($contents, [$i]);
$code .= 'return ' . var_export($contents, true) . ';';
file_put_contents($path, $code, LOCK_EX);

?>

7、参考:使用阿里云 性能测试 PTS 模拟并发 http 请求 。 重新并发测试后,再次查看程序运行结果。已经不存在 empty- 开头的文件,说明 使用 PHP ‘flock’ 函数显式获取读锁(共享锁定)有效。不过在程序运行期间,文件 return.php 的内容仍然是有为空的时候,只不过,此时,没有直接读取文件的内容罢了,而是等待中。如视频1

8、不过,查看测试报告,加了读锁之后,性能有所下降。加读锁前后的性能对比如下。加锁前的测试报告:平均RT(ms):11,TPS(平均/峰值):832/903。加锁后的测试报告:平均RT(ms):12,TPS(平均/峰值):825/872。名称解释:平均RT(ms)RT业务响应时间(Response Time),平均RT是所有API的RT平均值,单位为ms,愈小愈好。TPS(平均/峰值)TPS系统每秒处理事务数 Transaction Per Second),包括TPS的平均值和峰值:平均:表示压测周期内,该场景TPS的平均值。峰值:表示压测周期内,该场景的最高TPS,愈大愈好。如图2、图3

图2

 

图3

9、因此,是否加读锁,需要自行衡量了。如果不加读锁,建议在读取文件内容时,需要判断是否为空,如果为空,需要想办法额外处理。

永夜

View Comments