如何确保文件写入失败时的文件完整性?

时间:2009-01-20 19:22:13

标签: java file locking integrity

跟进How to safely update a file that has many readers and one writer?

在我之前的问题中,我发现你可以使用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库可以为我处理这个问题?

修改

好吧,我设法在第三次尝试中弄错了。当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 时,读者不会锁定临时值。它也不是简单的锁定临时文件的简单修复!这将导致作者和读者以不同的顺序获取锁,并可能导致死锁。这一直变得越来越复杂。这是我的第四次尝试,

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

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

编辑2

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

编辑3

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

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

{{1}}

锁定和释放保护文件,临时文件和复制标志。唯一的问题是现在读者锁不能共享,但它永远不会真的。读者总是有机会修改文件,因此首先制作可共享锁是错误的。

5 个答案:

答案 0 :(得分:1)

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

所以,假设您要更新“myfile.txt”:

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

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

答案 1 :(得分:0)

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

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

答案 2 :(得分:0)

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

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

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

答案 3 :(得分:0)

如果没有某些操作系统支持,就不可能将任意复杂的文件操作设置为原子操作,而不需要打开文件的程序来完成或回滚可能正在进行的操作。如果这个警告是可以接受的,可以采取以下措施:

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

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

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

答案 4 :(得分:0)

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