【问题标题】:How to copy multiple files atomically from src to dest in java?java - 如何在java中以原子方式将多个文件从src复制到dest?
【发布时间】:2023-03-22 18:10:02
【问题描述】:

在一个要求中,我需要将多个文件从一个位置复制到另一个网络位置。

假设我在/src 位置存在以下文件。
a.pdf, b.pdf, a.doc, b.doc, a.txt and b.txt

我需要立即将a.pdf, a.doc and a.txt 文件atomically 复制到/dest 位置。

目前我正在使用 Java.nio.file.Files 包和代码如下

Path srcFile1 = Paths.get("/src/a.pdf");
Path destFile1 = Paths.get("/dest/a.pdf");

Path srcFile2 = Paths.get("/src/a.doc");
Path destFile2 = Paths.get("/dest/a.doc");

Path srcFile3 = Paths.get("/src/a.txt");
Path destFile3 = Paths.get("/dest/a.txt");

Files.copy(srcFile1, destFile1);
Files.copy(srcFile2, destFile2);
Files.copy(srcFile3, destFile3);

但是这个过程文件是一个接一个地复制的。
作为对此的替代,为了使整个过程成为原子的, 我正在考虑压缩所有文件并移动到/dest 并在目的地解压缩。

这种方法是否可以使整个复制过程成为原子的?任何人都遇到过类似的概念并解决了它。

【问题讨论】:

  • 您也可以一个一个地复制它们,首先使用 .tmp 文件扩展名,然后重命名它们。但是你的目标是什么?
  • @J.Doe 一个一个地复制多个文件不是原子操作权。考虑一个事务,其中多个表的数据存储在一个镜头中,我想要在这里。
  • 我不认为有一种方法可以获得纯粹的原子性,保证你要么得到你想要的东西,要么对文件系统没有任何变化。但是你可以做一些类似@J.Doe 建议的事情。我有类似的想法,但我的想法是首先将文件复制到您真正想要复制它们的目录中的隐藏目录。然后你会在复制后将它们移动到位。您可以非常有信心这 3 个动作会成功并快速奏效,但仍然会有很短的时间,其中只有 1 或 2 个文件。
  • 如果您希望上述场景作为单个事务发生,我建议您使用 Java 8 中引入的 Stream API。首先,为每个文件插入字节流,以Stream 对象中的字符,然后通过网络发送它。到达目标位置后,您可以遍历 Stream 对象并将每个字节流插入到一个位置。此外,如果您需要维护文件格式(.docx、.pdf、.txt),您应该使用定义为Map<ByteArrayInputStream, String> 的 Map 对象并将此 Map 对象作为 Stream 对象通过网络发送。
  • 您的 zip 和 copy 解决方案是正确且原子的,因此当出现单个问题时,目标目录中不会出现任何文件。只要确保你在临时目录中压缩你的文件

标签: java spring java.nio.file


【解决方案1】:

这种方法是否可以使整个复制过程成为原子的?任何人都遇到过类似的概念并解决了它。

您可以将文件复制到一个新的临时目录,然后重命名该目录。

在重命名临时目录之前,您需要删除目标目录

如果您不想覆盖的目标目录中已经有其他文件,您可以将临时目录中的所有文件移动到目标目录。

然而,这不是完全原子的。

删除 /dest:

String tmpPath="/tmp/in/same/partition/as/source";
File tmp=new File(tmpPath);
tmp.mkdirs();
Path srcFile1 = Paths.get("/src/a.pdf");
Path destFile1 = Paths.get(tmpPath+"/dest/a.pdf");

Path srcFile2 = Paths.get("/src/a.doc");
Path destFile2 = Paths.get(tmpPath+"/dest/a.doc");

Path srcFile3 = Paths.get("/src/a.txt");
Path destFile3 = Paths.get(tmpPath+"/dest/a.txt");

Files.copy(srcFile1, destFile1);
Files.copy(srcFile2, destFile2);
Files.copy(srcFile3, destFile3);
delete(new File("/dest"));
tmp.renameTo("/dest");
void delete(File f) throws IOException {
  if (f.isDirectory()) {
    for (File c : f.listFiles())
      delete(c);
  }
  if (!f.delete())
    throw new FileNotFoundException("Failed to delete file: " + f);
}

只需覆盖文件:

String tmpPath="/tmp/in/same/partition/as/source";
File tmp=new File(tmpPath);
tmp.mkdirs();
Path srcFile1 = Paths.get("/src/a.pdf");
Path destFile1=paths.get("/dest/a.pdf");
Path tmp1 = Paths.get(tmpPath+"/a.pdf");

Path srcFile2 = Paths.get("/src/a.doc");
Path destFile2=Paths.get("/dest/a.doc");
Path tmp2 = Paths.get(tmpPath+"/a.doc");

Path srcFile3 = Paths.get("/src/a.txt");
Path destFile3=Paths.get("/dest/a.txt");
Path destFile3 = Paths.get(tmpPath+"/a.txt");

Files.copy(srcFile1, tmp1);
Files.copy(srcFile2, tmp2);
Files.copy(srcFile3, tmp3);

//Start of non atomic section(it can be done again if necessary)

Files.deleteIfExists(destFile1);
Files.deleteIfExists(destFile2);
Files.deleteIfExists(destFile2);

Files.move(tmp1,destFile1);
Files.move(tmp2,destFile2);
Files.move(tmp3,destFile3);
//end of non-atomic section

即使第二种方法包含一个非原子部分,复制过程本身也会使用一个临时目录,这样文件就不会被覆盖。

如果在移动文件期间进程中止,则可以轻松完成。

请参阅https://stackoverflow.com/a/4645271/10871900 作为移动文件的参考,https://stackoverflow.com/a/779529/10871900 作为递归删除目录的参考。

【讨论】:

    【解决方案2】:

    首先,复制文件或目录有多种可能性。 Baeldung 很好地洞察了不同的可能性。此外,您还可以使用 Spring 的 FileCopyUtils。不幸的是,所有这些方法都不是原子的。

    我找到了一个older post 并对其进行了一些调整。您可以尝试使用低级事务管理支持。这意味着您从该方法中创建一个事务并定义在回滚中应该执行的操作。 Baeldung还有一篇不错的文章。

    @Autowired
    private PlatformTransactionManager transactionManager;
    
    @Transactional(rollbackOn = IOException.class)
    public void copy(List<File> files) throws IOException {
        TransactionDefinition transactionDefinition = new DefaultTransactionDefinition();
        TransactionStatus transactionStatus = transactionManager.getTransaction(transactionDefinition);
    
        TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
    
            @Override
            public void afterCompletion(int status) {
                if (status == STATUS_ROLLED_BACK) {
                    // try to delete created files
                }
            }
        });
    
        try {
            // copy files
            transactionManager.commit(transactionStatus);
        } finally {
            transactionManager.rollback(transactionStatus);
        }
    }
    

    或者你可以使用一个简单的try-catch-block。如果抛出异常,您可以删除创建的文件。

    【讨论】:

      【解决方案3】:

      您的问题缺乏原子性目标。即使解压缩也不是原子的,VM 可能会在膨胀第二个文件的块之间发生 OutOfMemoryError 崩溃。所以有一个文件完整,第二个没有,第三个完全丢失。

      我唯一能想到的是两阶段提交,就像所有带有临时目的地的建议突然变成真正的目标一样。这样您就可以确定,第二个操作要么永远不会发生,要么创建最终状态。

      另一种方法是之后在目标中编写一种廉价的校验和文件。这将使外部进程可以轻松地侦听此类文件的创建并使用找到的文件验证其内容。

      后者与立即提供容器/ ZIP/ 存档而不是将文件堆积在目录中相同。大多数档案都有或支持完整性检查。

      (如果目录或文件夹在写入时消失,操作系统和文件系统的行为也会有所不同。有些人接受它并将所有数据写入可恢复的缓冲区。其他人仍然接受写入但不更改任何内容。其他人一开始就立即失败写入,因为设备上的目标块是未知的。)

      【讨论】:

        【解决方案4】:

        原子写入:

        标准文件系统没有原子性概念,因此您只需要执行单个操作 - 那将是原子的。

        因此,为了以原子方式写入更多文件,您需要创建一个文件夹,例如,名称中包含时间戳,并将文件复制到该文件夹​​中。

        然后,您可以将其重命名为最终目的地或创建符号链接。

        您可以使用与此类似的任何东西,例如 Linux 上基于文件的卷等。

        请记住,删除现有符号链接并创建新符号链接永远不会是原子的,因此您需要处理代码中的情况,并在重命名/链接文件夹可用时切换到该文件夹​​,而不是删除/创建链接。但是,在正常情况下,删除和创建新链接是一个非常快的操作。

        原子读取:

        嗯,问题不在于代码,而在于操作系统/文件系统级别。

        前段时间,我遇到了一个非常相似的情况。有一个数据库引擎正在运行并“一次”更改多个文件。我需要复制当前状态,但在复制第一个文件之前,第二个文件已经更改。

        有两种不同的选择: 使用支持快照的文件系统。有时,您会创建一个快照,然后从中复制文件。 您可以使用fsfreeze --freeze 锁定文件系统(在Linux 上),稍后使用fsfreeze --unfreeze 解锁。当文件系统被冻结时,您可以照常读取文件,但没有进程可以更改它们。

        这些选项都不适合我,因为我无法更改文件系统类型,并且无法锁定文件系统(它是根文件系统)。

        我创建了一个空文件,将其挂载为loop 文件系统,并对其进行了格式化。从那一刻起,我可以fsfreeze 只是我的虚拟卷而不接触根文件系统。

        我的脚本首先调用fsfreeze --freeze /my/volume,然后执行复制操作,然后调用fsfreeze --unfreeze /my/volume。在复制操作期间,无法更改文件,因此复制的文件都来自同一时间 - 就我而言,这就像一个原子操作。

        顺便说一句,一定不要fsfreeze 你的根文件系统:-)。我做到了,重启是唯一的解决方案。

        类似数据库的方法:

        即使是数据库也不能依赖原子操作,因此它们首先将更改写入 WAL(预写日志)并将其刷新到存储中。刷新后,他们可以将更改应用到数据文件。

        如果有任何问题/崩溃,数据库引擎首先加载数据文件并检查WAL中是否有一些未应用的事务并最终应用它们。

        这也称为日志,被一些文件系统(ext3、ext4)使用。

        【讨论】:

          【解决方案5】:

          我希望这个解决方案有用:根据我的理解,您需要将文件从一个目录复制到另一个目录。 所以我的解决方案如下: 谢谢。!!

          公共类 CopyFilesDirectoryProgram {

          public static void main(String[] args) throws IOException {
              // TODO Auto-generated method stub
              String sourcedirectoryName="//mention your source path";
              String targetdirectoryName="//mention your destination path";
              File sdir=new File(sourcedirectoryName);
              File tdir=new File(targetdirectoryName);
              //call the method for execution
              abc (sdir,tdir);
          
          }
          
          private static void abc(File sdir, File tdir) throws IOException {
              
              if(sdir.isDirectory()) {
                  copyFilesfromDirectory(sdir,tdir);
              }
                  else
                  {
                      Files.copy(sdir.toPath(), tdir.toPath());
                  }
              }
          
          
          private static void copyFilesfromDirectory(File source, File target) throws IOException {
              
              if(!target.exists()) {
                  target.mkdir();
                  
              }else {
                  for(String items:source.list()) {
                      abc(new File(source,items),new File(target,items));
                  }
              }
          }
          

          }

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 2018-06-01
            • 2011-06-09
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2016-10-11
            • 1970-01-01
            相关资源
            最近更新 更多