4

跟进如何安全地更新一个有许多读者和一个作者的文件?

在我之前的问题中,我发现您可以使用 FileChannel 的锁来确保读取和写入的顺序。

但是,如果编写器在写入过程中失败(比如 JVM 崩溃),您将如何处理这种情况?这个基本算法看起来像,

WRITER:
  lock file
  write file
  release file

READER:
  lock file
  read file
  release file

如果 JVM 在 期间崩溃write file,肯定会释放锁,但现在我有一个不完整的文件。我想要完整的东西总是可读的。要么是旧内容,要么是新内容,两者之间没有任何内容。

我的第一个策略是写入一个临时文件,然后将内容复制到“实时”文件中(同时确保良好的锁定)。算法是,

WRITER:
  lock temp file
  write temp file
  lock file
  copy temp to file
  release file
  release temp
  delete temp

READER:
  lock file
  read file
  release file

一件好事是,delete temp如果临时文件已被另一位作家锁定,则不会删除临时文件。

但是如果 JVM 在copy temp to file. 然后我添加了一个copying标志,

WRITER:
  lock temp file
  write temp file
  lock file
  create copying flag
  copy temp to file
  delete copying flag
  release file
  release temp
  delete temp

READER:
  lock file
  if copying flag exists
    copy temp to file
    delete copying flag
    delete temp 
  end
  read file
  release file

永远不会有两件事访问该copying文件,因为它受到文件锁的保护。

现在,这是这样做的方法吗?确保非常简单的事情似乎很复杂。是否有一些 Java 库可以为我处理这个问题?

编辑

好吧,我设法在第三次尝试中犯了一个错误。阅读器不会将锁锁定到 temp copy temp to file。简单地锁定临时文件也不是一个简单的修复!这将导致写入者和读取者以不同的顺序获取锁,并可能导致死锁。这一直在变得越来越复杂。这是我的第四次尝试,

WRITER:
  lock file
  write temp file
  create copying flag
  copy temp to file
  delete copying flag
  delete temp
  release file

READER:
  lock file
  if copying flag exists
    copy temp to file
    delete copying flag
    delete temp 
  end
  read file
  release file

这次临时文件由主锁保护,因此它甚至不需要自己的锁。

编辑 2

当我说 JVM 崩溃时,我的意思是说电源坏了,而你没有 UPS。

编辑 3

我仍然设法犯了另一个错误。您不应该锁定您正在写入或读取的文件。这将导致问题,因为除非您在 Java 中使用 RandomAccessFile,否则您无法同时获得读取和写入锁,它不实现输入/输出流。

相反,您要做的只是锁定一个锁定文件,该锁定文件保护您正在读取或写入的文件。这是更新的算法:

WRITER:
  lock
  write temp file
  create copying flag
  copy temp to file
  delete copying flag
  delete temp
  release

READER:
  lock
  if copying flag exists
    copy temp to file
    delete copying flag
    delete temp 
  end
  read file
  release

锁定和释放保护文件、临时文件和复制标志。唯一的问题是现在不能共享读卡器锁,但它永远不可能真正共享。读者总是有机会修改文件,因此一开始就创建一个可共享的锁是错误的。

4

5 回答 5

1

尽管没有防弹、跨操作系统、跨 FS 解决方案,但“写入唯一临时文件并重命名”策略仍然是您的最佳选择。大多数平台/文件系统都试图使文件重命名(有效地)原子化。请注意,您想使用 +separate+ 锁定文件进行锁定。

所以,假设你想更新“myfile.txt”:

  • 锁定“myfile.txt.lck”(这是一个单独的空文件)
  • 将更改写入唯一的临时文件,例如“myfile.txt.13424.tmp”(使用File.createTempFile()
  • 为获得额外保护,但可能更慢,请在继续 ( ) 之前 fsync 临时文件FileChannel.force(true)
  • 将“myfile.txt.13424.tmp”重命名为“myfile.txt”
  • 解锁“myfile.txt.lck”

在某些平台(windows)上,由于文件操作的限制,您需要做更多的跳舞(您可以在重命名之前将“myfile.txt”移动到“myfile.txt.old”,并使用“.old”文件恢复如果您在阅读时需要的话)。

于 2011-04-04T17:26:57.073 回答
0

我不认为有一个完美的答案。我不完全知道您需要做什么,但是您可以写入一个新文件,然后在成功时重命名文件,而不是复制。重命名很快,因此应该不太容易崩溃。如果在重命名阶段失败,这仍然无济于事,但您已将风险窗口降至最低。

同样,我不确定它是否适用或与您的需求相关,但是您能否在文件末尾写一些文件块以显示所有数据已写入?

于 2009-01-20T19:39:46.600 回答
0

我假设您有一个不断追加的大文件。虚拟机崩溃并不经常发生。但如果它们发生,您需要一种方法来回滚失败的更改。您只需要一种方法来知道回滚多远。例如,通过将最后一个文件长度写入新文件:

WRITER:
  lock file
  write file position to pos-file
  write file
  remove pos-file
  unlock file

如果作者崩溃,您的一位读者将获得读锁。他们必须检查 pos 文件。如果他们找到一个,就会发生崩溃。如果他们查看文件内部,他们知道将更改回滚到多远才能再次获得一致的文件。当然,回滚过程必须以与写入过程类似的方式发生。

当您不是追加而是替换文件时,您可以使用相同的方法:

WRITER:
  lock file
  write writing-in-progress-file
  write file
  remove writing-in-progress-file
  unlock file

与以前适用于读者的规则相同。当正在写入的文件存在但读取器已经获得读锁时,写入的文件处于不一致状态。

于 2009-01-20T22:09:00.137 回答
0

如果没有一些操作系统支持,那么在不要求打开文件的程序完成或回滚可能正在进行的操作的情况下,将不可能使任意复杂的文件操作成为原子操作。如果该警告是可以接受的,则可以执行以下操作:

  1. 在文件开头附近,不跨越 512 字节边界,包括两个 4 或 8 字节数字(取决于最大文件大小),指示文件的逻辑长度和更新记录的位置(如果有) . 大多数时候更新记录的值为零;写入非零值的行为将提交更新序列;当更新序列完成时,它将被重写为零。
  2. 在更新序列开始之前,确定更新序列完成时文件逻辑长度的上限(有一些方法可以绕过这个限制,但它们会增加复杂性)
  3. 要开始更新序列,请在文件中寻找其当前逻辑长度或未来逻辑长度(上限)中较大的距离,然后写入更新记录,每个更新记录由文件偏移量、字节数组成要写入,以及要写入的数据。写入执行所有更新所需的尽可能多的此类记录,并以具有 0 偏移量和 0 长度的记录结束。
  4. 要提交更新序列,请刷新所有挂起的写入并等待它们完成,然后写入新文件长度和第一条更新记录的位置。
  5. 最后,通过依次处理所有更新记录、刷新所有写入并等待其完成来更新文件,并将更新记录位置设置为零,并(可选)将文件截断为其逻辑长度。
  6. 如果尝试打开更新序列位置非零的文件,请在执行任何其他操作之前完成执行任何挂起的写入(使用上述最后一步)。

如果在写入更新记录位置之前写入文件的原始操作失败,则所有写入操作都将被有效忽略。如果在写入更新记录位置之后但在清除之前失败,则下次打开文件时将提交所有写入操作(其中一些可能已经执行,但再次执行应该是无害的) . 如果在更新记录位置写入后失败,则文件更新将完成,失败完全不会影响它。

其他一些方法使用单独的文件来保存挂起的写入。在某些情况下,这可能是个好主意,但它的缺点是将文件分成两部分,两部分必须放在一起。仅复制一个文件,或意外配对在不同时间制作的两个文件的副本,可能会导致数据丢失或损坏。

于 2011-04-04T16:26:16.590 回答
0

解决方案:使用 2 个文件并按连续顺序写入它们。
只有 1 次写入会失败。

于 2011-05-18T13:39:32.053 回答