PHP file_put_contents文件锁定


9

Senario:

您有一个文件,每行上都有一个字符串(平均句子价值)。为了争辩,可以说这个文件的大小为1Mb(几千行)。

您有一个脚本来读取文件,更改文档中的某些字符串(不仅是追加,而且还要删除和修改某些行),然后用新数据覆盖所有数据。

问题:

  1. “服务器” PHP,OS或httpd等是否已经有适当的系统来阻止此类问题(在写入过程中进行读取/写入)?

  2. 如果可以,请说明其工作原理,并提供示例或相关文档的链接。

  3. 如果没有,我是否可以启用或设置某些东西,例如将文件锁定直到写入完成,并且使所有其他读取和/或写入失败,直到上一个脚本完成写入?

我的假设和其他信息:

  1. 该服务器正在运行PHP和Apache或Lighttpd。

  2. 如果脚本是由一个用户调用的,并且正在写入文件的一半,而另一位用户在该确切时刻读取了文件。阅读该文档的用户将无法获得完整的文档,因为该文档尚未编写。(如果这个假设是错误的,请纠正我)

  3. 我只关心PHP写入和读取文本文件,尤其是函数“ fopen” /“ fwrite”,主要是“ file_put_contents”。我查看了“ file_put_contents”文档,但未找到详细程度或对“ LOCK_EX”标志的含义或作用的很好解释。

  4. 该方案是最坏情况的一个示例,在该示例中,我认为由于文件的大尺寸和数据的编辑方式,这些问题更有可能发生。我想了解更多有关这些问题的信息,不需要或不需要诸如“使用mysql”或“为什么要这么做”之类的答案或注释,因为我没有这样做,我只想了解文件读/写使用PHP,似乎并没有找到正确的位置/文档,是的,我理解PHP并不是用这种方式处理文件的理想语言。


2
我可以从经验中告诉您,使用PHP读取和写入大文件(1 MB确实不是那么大,但是仍然)可能会比较棘手(而且速度很慢)。您可以始终锁定文件,但是仅使用数据库可能会更轻松,更安全。
NullUserException 2012年

我知道使用数据库会更好。请阅读问题(最后一段4)
hozza 2012年

2
我确实读过这个问题;我是说这不是一个好主意,还有更好的选择。
NullUserException 2012年

2
file_put_contents()只是fopen()/fwrite()舞蹈的包装,LOCKEX就像调用一样flock($handle, LOCKEX)
扬尼斯,2012年

2
这就是为什么我发布评论而不是答案的原因。
NullUserException 2012年

Answers:


4

1)不3)不

最初建议的方法存在几个问题:

首先,某些类似UNIX的系统(例如Linux)可能没有实现锁定支持。操作系统默认不锁定文件。我已经看到syscall是NOP(无操作),但是那是几年前的事了,所以您需要验证应用程序实例设置的锁是否受到另一个实例的尊重。(即2个同时访问者)。如果仍未实现锁定(很可能是),则操作系统允许您覆盖该文件。

出于性能原因,逐行读取大文件是不可行的。我建议使用file_get_contents()将整个文件加载到内存中,然后对其进行explode()以获得行。或者,使用fread()读取块中的文件。目的是最大程度地减少读取调用的次数。

关于文件锁定:

LOCK_EX表示排他锁(通常用于写入)。在给定时间,只有一个进程可以为给定文件持有排他锁。LOCK_SH是一个共享锁(通常用于读取),一个以上的进程可能会在给定时间为给定文件持有一个共享锁。LOCK_UN解锁文件。如果您使用file_get_contents()http://en.wikipedia.org/wiki/File_locking#In_Unix-like_systems,则自动完成解锁

优雅的解决方案

PHP支持数据流过滤器,该过滤器旨在处理文件或其他输入中的数据。您可能希望使用标准API正确创建一个这样的过滤器。 http://php.net/manual/en/function.stream-filter-register.php http://php.net/manual/en/filters.php

替代解决方案(3个步骤):

  1. 创建一个队列。不用处理一个文件名,而是使用数据库或其他机制将唯一的文件名存储在挂起的/中,并在/ processed中进行处理。这样,任何内容都不会被覆盖。该数据库还将用于存储其他信息,例如元数据,可靠的时间戳,处理结果等。

  2. 对于不超过几MB的文件,将整个文件读入内存,然后对其进行处理(file_get_contents()+ explode()+ foreach())

  3. 对于较大的文件,以块为单位读取文件(即1024字节),并在读取时实时处理+写入每个块(注意最后一行不以\ n结尾的行,需要在下一批中进行处理)


1
“我已经看到系统调用是NOP(无操作)...”哪个内核?
Massimo

1
“出于性能方面的考虑,逐行读取大文件是不可行的。我建议使用file_get_contents()将整个文件加载到内存中……”这是毫无意义的。我可以说:出于性能原因,请勿将大文件读入内存...该做什么取决于许多其他因素。
Massimo

4

我知道这已经很老了,但万一有人碰到这个。恕我直言,解决方法是这样的:

1)使用file_get_contents('original.txt')打开原始文件(例如original.txt)。

2)进行更改/编辑。

3)使用file_put_contents('original.txt.tmp')并将其写入临时文件original.txt.tmp。

4)然后将tmp文件移至原始文件,替换原始文件。为此,请使用rename('original.txt.tmp','original.txt')。

优点:在处理文件并将其写入文件时,该文件未锁定,其他人仍可以读取旧内容。至少在Linux / Unix上,重命名是原子操作。文件写入过程中的任何中断都不会影响原始文件。只有将文件完全写入磁盘后,才会移动文件。在http://php.net/manual/en/function.rename.php的注释中对此进行了更有趣的阅读

编辑以解决评论(太多评论):

/programming/7054844/is-rename-atomic进一步引用了在跨文件系统进行操作时可能需要执行的操作。

关于读取共享锁,我不确定为什么需要这样做,因为在此实现中,没有直接写入文件。PHP的羊群(用于获取锁)有点但不可靠,可以被其他进程忽略。这就是为什么我建议使用重命名的原因。

理想情况下,重命名文件应该是重命名进程唯一的名称,以确保没有2个进程执行相同的操作。但是,这当然不能防止多个人同时编辑同一文件。但是至少该文件将保持原样(最后编辑将获胜)。

步骤3)和4)将变为:

$tempfile = uniqid(microtime(true)); // make sure we have a unique name
file_put_contents($tempFile); // write temp file
rename($tempfile, 'original.txt'); // ideally on the same filesystem

正是我也想提出的建议。但是我也会在读取时获得一个共享锁,以防止数据崩溃。
marco-a

重命名是在同一磁盘上而不是在不同磁盘上的原子操作。
Xnoise

真正保证唯一的临时文件名,你也可以使用tempnam功能,这原子创建一个文件,返回文件名。
Matthijs Kooijman,

1

在PHP文档中的file_put_contents()中,您可以在example#2中找到LOCK_EX的用法,简单地说:

file_put_contents('somefile.txt', 'some text', LOCK_EX);

所述LOCK_EX是具有恒定的整数比可以在某些函数中使用的值按位

为了控制文件锁定,还有一个特定的函数:flock()方式。


尽管这很有趣,并且在某些情况下可能有用,但是在读取,修改和重写文件时,应在读取文件之前先获取该锁,并保持该锁直到其被完全重写(否则,其他过程可能会读取旧副本并进行更改)过程完成后返回)。我不相信这可以用实现file_get/put_contents
Jules

0

您没有提到的一个问题,您也需要注意的是竞态条件,其中脚本的两个实例几乎同时运行,例如,出现的顺序如下:

  1. 脚本实例1:读取文件
  2. 脚本实例2:读取文件
  3. 脚本实例1:将更改写入文件
  4. 脚本实例2:使用自己的更改将第一个脚本实例的更改覆盖到文件中(因为这时其读取已过时)。

因此,在更新大文件时,需要先对该文件进行LOCK_EX读取,然后再进行写操作才释放锁。在此示例中,我认为这将导致第二个脚本实例在等待轮流访问文件时挂起一会儿,但这比丢失数据更好。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.